Composable Storefront supports Server-side rendering (SSR) which refers to the process of rendering the requested page on the server side rather than on the client side with Client-side rendering (CSR). With SSR the page is pre-rendered on the server before being sent to the client’s browser. A Node.js server is responsible for handling requests, rendering the page on the server side, and sending back the pre-rendered content to the client. SSR in Composable Storefront consists of the following steps:

    1. When a page request hits the Node.js server the rendering engine of the application decides based on the renderingStrategyResolver if this request will be renderer on the server with SSR or if the request should be rendered with CSR.

 

    1. When the strategy determined SSR, the Node.js server and the OptimizedSsrEngine take care of rendering the page on the server. There are various options available to configure the OptimizedSrrEngine.

 

    1. The engine bootstraps, similar to a browser, the app and starts initializing the app and component rendering which also includes data fetching from APIs (e.g. SAP Commerce Cloud OCC Api). In simpliefied terms it generates HTML which already contains content fetched from APIs and translations. It can also contain SSR Transfer State.

 

    1. When enabled (inlineCriticalCss: true), the engine will also start extracting and inlining the critical CSS which refers to the minimal set of CSS styles required to render the above-the-fold content of the requested page.

 

    1. The server sends the pre-rendered HTML along with any necessary styles and scripts to the client as the response to the request.

When the rendering finished before the timeout happens, the server will log the following message:

Request is resolved with the SSR rendering result ([/some/url])

 

The SSR timeout

When the engine is not able to render the requested page (which can also include SSR Transfer State and inline styles), after a given timeout value (which can be configured in Composable Storefront), the server falls back to the CSR scenario where it returns the index.html file and the rendering is handled in the client’s browser. In this case, in the server’s log you can see log messages that notify about this scenario happend:

SSR rendering exceeded timeout [e.g., 5000], fallbacking to CSR for [/some/url]

Nevertheless, the server continues rendering that page in the background and with that, the next request for that specific page that hits the server, can benefit from the meanwhile finished rendering or it can reuse a rendering that is still ongoing.

When the rendering finished in the background the server will log:

Request is resolved with the SSR rendering result ([/some/url])

If the server is still not able to finish the rendering with a configured period of time (maxRenderTime) it will log the following:

Rendering of [/some/url] was not able to complete. This might cause memory leaks!

For reference: Incomplete Renders and Memory leaks (help.sap.com)

The timed out request and how the trace looks in Dynatrace

When the fallback to CSR happens, the server will send a response (which is the CSR page) and therefore Dynatrace stops capturing the request. It does not capture the rendering anymore which now happens in the background.

This is how the trace in Dynatrace in such a case can look like.

 

Distrubed trace of a request that resulted in a CSR fallback scenario

The trace does not give any information about what happened in the red marked area / period.

Common issues

Most likely the SSR timeout happens because of one of the following issues:

    • forever ticking setInterval()

 

    • recursively-called setTimeout()

 

    • One or more external calls to API or lazy loaded translation files are still not finished

 

    • Pending connections (e.g. SNAT port exhaustion)

 

    • The extraction and inline of critical CSS consumes to much time

But i can be also due to other asynchronous task that not completed yet.

setInterval() & recursively-called setTimeout()

Check your code for occurences of such scenarios and either fix them or exclude them from SSR.

External calls

Overall, the general rule is to avoid any unnecessary or long running calls in SSR.

Obviously a call to an external system that takes e.g. 5 seconds will not allow the rendering to finish within any time below 5 seconds. In some scenarios you can easily identify such calls when the page renders in CSR and you can observe slow calls in the browser’s developer tools. In that case you may want to optimize the response time of those requests (e.g. by caching them) or you exclude them from the SSR (e.g. requests that return personalized data or that are not relevant for SSR at all).

If you have implemented lazy loading of translation files in the assets of the storefront app itself, enable the loading from local instead of fetching files via HTTP requests in SSR.

Pending connections

A pending connection refers to an external call that is not yet accepted by the target system. Such a scenario can happen if you face infrastructure limitations such as SNAT Port exhaustion. It is not guaranteed that this connection will finish at any time in future. In that case you may want to timeout the outgoing connection which is configurable in Composable Storefront via Configurable Timeouts for Outgoing HTTP Requests in SSR.

Inling of critical CSS

Setting inlineCriticalCss:true in the context of Angular Universal server-side rendering (SSR) offers several benefits related to the initial rendering performance of a web page, such as:

    • Reduced Render Blocking, leads to a faster rendering of the initial content, as the browser can start rendering the page with the necessary styles immediately.

 

    • Minimize the time it takes for users to see the initial content of a page.

 

    • Search Engine Optimization (SEO): by optimizing the initial rendering through inlining critical CSS, you may indirectly improve the SEO of your web pages.

However, the extraction and inling of critical CSS can be a very time consuming task which depends on the following two values:

    1. The amount of DOM elements which the algorithm needs to parse and traverse

 

    1. The amound of CSS selectors which the algorithm needs to traverse

You can simply check how long this process for a given pages takes by comparing the timings of the rendering when inlineCriticalCss: true and inlineCriticalCss: false is set in server.ts file.

Depending on your DOM size and the amount of CSS selectors this process can take multiple seconds. In that case your options are:

    1. Reduce the DOM size in SSR, e.g.: remove content that is not visible in the beginning and also not SEO relevant. A good example for this is the main navigation which may contains several levels but only the first level is initially visible.

 

    1. Reduce the amount of CSS selectors:
        1. remove unused styles whenever possible by carefully importing 3rd party style libraries (only import those components of the libraries that you really need).
        1. Skip styles of Composable Storefront ootb. Components that you do not use at all.
        1. For Composable Storefront Page Templates that you do not use at all, use $page-template-blocklist to skip those for styles generation.

Implementing those countermeasure can also help improving you CSR performance.

Track/Log asynchronous tasks

In your Composable Storefront app you can track async tasks that are currently involved right after the timeout happens.

Therefore you can add ngx-zone-task-tracking as dependency (package.json):

 

package.json

and configure the logging by importing the ZoneTaskTrackingModule into the apps AppServerModule:

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ServerTransferStateModule,
    ZoneTaskTrackingModule.printWithDelay(5050)
  ],
  providers: [
    ...
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

Please note: please adopt the value of the printWithDelay paramater to a value that is +50ms of the configured timeout value.

The logging will then directly after the timeout happend log the pending tasks, which looks like this:

 Pending tasks in NgZone:
 {
  macroTasks: [
    {
      stacktrace: Error: Task 'macroTask' from 'ZoneMacroTaskWrapper.subscribe'.
          at TaskTrackingZoneSpec.onScheduleTask (/storefront/server/main.js:366991:36)
          at _ZoneDelegate.scheduleTask (/storefront/server/main.js:364813:45)
          at Object.onScheduleTask (/storefront/server/main.js:364725:25)
          at _ZoneDelegate.scheduleTask (/storefront/server/main.js:364813:45)
          at Zone.scheduleTask (/storefront/server/main.js:364663:37)
          at Zone.scheduleMacroTask (/storefront/server/main.js:364685:21)
          at Observable._subscribe (/storefront/server/main.js:110282:40)
          at Observable._trySubscribe (/storefront/server/main.js:341073:25)
          at Observable.subscribe (/storefront/server/main.js:341059:22)
          at innerSubscribe (/storefront/server/main.js:342076:23),
      _task: [ZoneTask]
    }
  ],
  microTasks: []
}

In the above example we can identify a subscription that is pending right after the timeout happened.

Useful SSR logging options

Although there are various scenarios where bad code or bad performance of the application itself causes performance issues and SSR timeouts it’s always good to identify slow API requests in order to improve overall performance.

A tooling, already embedded into the Composable Storefront, is the Configurable Timeouts for Outgoing HTTP Requests in SSR. The default outgoing request timeout in SSR is set to 20 seconds but you can easily change this value. In case a slow running API request occurs it will log the following message:

Request to URL ‘${request.url}’ exceeded expected time of ${timeoutValue}ms and was aborted.

Also in the latest releases of Composable Storefront there are a few more options to improve Standardized SSR Logging which also includes the support of W3C trace contexts and connecting logs to distributed traces of SAP Commerce Cloud CCv2 Dynatrace.

If you need further options, you can consider to implement custom logging, e.g. an HttpInterceptor that logs HTTP request that finish after the SSR server already returned the CSR response when it decided to use the fallback mechanism. Please find below an example for such an HttpInterceptor:

import {HttpEvent, HttpEventType, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';
import {tap} from 'rxjs/operators';
import {Inject, Injectable, Optional} from '@angular/core';
import {REQUEST, RESPONSE} from '@nguniversal/express-engine/tokens';
import {Response} from 'express';
 
@Injectable({
  providedIn: 'root'
})
export class DebugHttpInterceptor implements HttpInterceptor {
 
  constructor(@Inject(REQUEST) private request: Request, @Optional() @Inject(RESPONSE) private response: Response) {
  }
 
  logBeforeTimeout = false;
 
  id = null;
 
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    let startTime = Date.now();
    return next.handle(req).pipe(
      tap(
        _event => {
          if(_event.type == HttpEventType.Response && this.response?.headersSent)
          {
            //log request that finish after node server sent already response which is the case if fallback happened
            console.log('Request - ' + req.url + ' FINISHED AFTER TIMEOUT - time in ms:' + (new Date().valueOf() - startTime.valueOf()) );
          } else if (_event.type == HttpEventType.Response && this.logBeforeTimeout && !this.response?.headersSent)
          {
            console.log('Request - ' + req.url + ' FINISHED BEFORE TIMEOUT - time in ms:' + (new Date().valueOf() - startTime.valueOf()) );
          }
        },
        _error => {
          console.log('Request -  :' + this.request.url + ' - ' + req.url + ' - FAILED');
        }
      )
    );
  }
}

Please note: you can also set logBeforeTimeout = true. With this, the interceptor will also log requests that finished before the timeout happened.

The interceptor itself can be provided within the AppServerModule (in the app.server.module.ts) and therefore only affects SSR renderings but does not affect the CSR scenario.

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ServerTransferStateModule,
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: DebugHttpInterceptor, multi: true },
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}
Sara Sampaio

Sara Sampaio

Author Since: March 10, 2022

0 0 votes
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x