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:
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])
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)
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.
Most likely the SSR timeout happens because of one of the following issues:
But i can be also due to other asynchronous task that not completed yet.
Check your code for occurences of such scenarios and either fix them or exclude them from SSR.
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.
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.
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:
However, the extraction and inling of critical CSS can be a very time consuming task which depends on the following two values:
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:
Implementing those countermeasure can also help improving you CSR performance.
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.
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 {}