To handle each SSR request, Angular creates a separate, fresh
PlatformRef with its own platform’s Injector. Then the platform bootstraps the app module, which mounts the app component into the DOM (not real DOM, but DOM representation on the server, driven by the
domino DOM adapter). Recursively, all child components are also mounted. And when the app is stable (when all async tasks are finished, e.g. http calls or
setTimeouts), the DOM representation is serialized to a string and sent back in the response to the client. Then the
PlatformRef is destroyed. This causes cascade: destroying the app module and the app's root
Injector, all the services (also calling their
ngOnDestroy hook) and destroying the root component and recursively it's child components with their relevant DOM nodes (also calling their
Disclaimer: This article is based on the source code of Angular v14.2.7. The source code for other versions may differ.
When initializing the ExpressJS app (likely in the
server.ts file), we invoke once the
ngExpressEngine() function (from
@nguniversal/express-engine). Internally it creates just one instance of the Angular's class
CommonEngine for the whole NodeJs process. Later, all requests will be handled by the same shared
For handling each SSR request, the method
SharedEngine.render() is called and when the returned
Promise resolves, the result HTML is returned in response to the client by passing the HTML to a special
callback of ExpressJS.
But how the result HTML is produced?
Side note: it was surprising for me that the essence of the server side rendering happens in the Angular's package
@angular/platform-server, but not in the
@nguniversal/express-engine; the latter is just a thin adapter for plugging the Angular's rendering into the ExpressJS server).
Then the app module is bootstrapped in this
PlatformRef, by calling
Side note: The phase of bootstrapping the app is very similar in the client side Angular:
platformBrowser().bootstrapModule(AppModule) (likely in
main.ts file) - it just uses a different platform object. But on the server, there might be many requests handled in parallel, and therefore many platforms (and their app modules) instantiated in parallel.
runs and awaits all the asynchronous APP_INITIALIZER hooks (by calling the method
ApplicationInitStatus.runInitializers()). Then it synchronously bootstraps the app component, which causes attaching it (and recursively it’s child components) into the DOM (however it's not a real DOM, but only a DOM representation on the server, driven by the
DominoAdapter class). Now, since all the
APP_INITIALIZERs completed and all the components were rendered for the first time the app is considered as fully bootstrapped.
Promise returned by
platformRef.renderModule(module) (which resolves when the app is fully bootstrapped) is passed into a function
_render(). This function waits for this
Promise to resolve. And then it waits until the application becomes stable, by subscribing to the observable
ApplicationRef.isStable and awaiting the first emitted
true value. It will emit
true, when all the pending asynchronous tasks in the app are completed (e.g. http calls to a backend API,
Side note: loading some data from backend via a http call to might result in updating some components and displaying important data on the page. Thats why Angular SSR waits for all async tasks to complete, to be sure that the app is stable. Only then the final HTML will look good.
Side note: we can hook into the moment before the app is serialized, by providing the public
BEFORE_APP_SERIALIZED. For example, the
TransferState module uses this hook to embed a JSON state as a
<script> tag into the document just before the app's serialization.
When the HTML response is ready, the instance of the platform (and it’s app) is not needed anymore. So then the
PlatformRef is destroyed. And this causes cascade: destroying the app module and the app's root
Injector, which causes destroying all the services (also calling their
ngOnDestroy hook) and eventually destroying the rendered root component and recursively destroying it's components subtree, with their relevant DOM nodes (also calling
ngOnDestroy hook for them).
Now all the objects created by the app are unused (unless the app had a memory leak). And after some time, Garbage Collector will clean up all those objects from the memory of the NodeJS process.
Knowing how Angular SSR works under the hood, we can deduce a few practical conclusions:
If the app has some forever pending async task (e.g. http call to a backend API that never responds), then the observable
ApplicationRef.isStable will never emit
true value. Therefore the rendering will never complete, so the client will never get a response. Moreover, the platform and the app will never be destroyed, and Garbage Collector will never clean up objects created by such an app. This causes a memory leak by itself. If your SSR never ends and you have no clue which pending async task causes it, see: How to find out why Angular SSR hangs - track NgZone tasks 🐾 .
When the app’s logic depends on some mutable global object (e.g. a global variable or a static property of a class), and when many applications are rendered in parallel on the server, they share the same global variable in the NodeJS process and are prone to race conditions when reading/writing to such a variable. For more, see: Don’t use global static objects - avoid race condition in SSR Angular 🏎
If any of the components subscribes to some data source (e.g. to a RxJs observable), but doesn’t unsubscribe (e.g. in
ngOnDestroy hook), then even after destroying the whole app, such a component will be considered as in use and the Garbage Collector will never clean it up. Therefore, even after the application is formally destroyed, the memory allocated for this component object is never released. It’s a memory leak. And going on, the more SSR requests and rendered apps with such a component’s logic, the more memory will be allocated in the NodeJs process and never released. Eventually, when the NodeJS process is out of memory, it will crash.
The same holds for services. If you subscribe to an observable in the service, make sure to unsubscribe later, e.g. in
ngOnDestroy method of the service. Although it doesn’t happen in the browser, on the server the
ngOnDestroy hook will be called on each service, when the application is destroyed. For more, see: ngOnDestroy in services - unsubscribe to avoid memory leaks in SSR Angular 💧