How Angular 14 SSR works under the hood - source code analysis 🕵️
Tags: angular, nguniversal, ssr, deepdive
Published: 2022-10-20 19:36:58

**To [handle each SSR request](https://github.com/angular/universal/blob/691e2fef638e0e8b5df6762684bbb76ddea79c48/modules/express-engine/src/main.ts#L36-L40), Angular [creates a separate, fresh `PlatformRef`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L153) with its own [platform’s Injector](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L322-L325). Then the [platform bootstraps the app module](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L154), which [mounts the app component into the DOM](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L506)  (not real DOM, but DOM representation on the server, driven by the [`domino` DOM adapter](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/domino_adapter.ts#L38)). Recursively, all child components are also mounted. And [when the app is stable](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L64-L66) (when all async tasks are finished, e.g. http calls or `setTimeout`s), the [DOM representation is serialized to a string](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L93) and sent back in the response to the client. Then the [`PlatformRef` is destroyed](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L94). This causes cascade: [destroying the app module](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L535-L545) and the [app's root `Injector`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/render3/ng_module_ref.ts#L97), all the [services (also calling their `ngOnDestroy` hook)](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/di/r3_injector.ts#L176-L178) and [destroying the root component](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L1068) and recursively it's child components with their relevant DOM nodes (also calling their `ngOnDestroy` hook).**

_Disclaimer: This article is based on the source code of Angular v14.2.7. The source code for other versions may differ._


## Setting up Angular ExpressJS engine

When initializing the ExpressJS app (likely in the `server.ts` file), we [invoke _once_ the `ngExpressEngine()`](https://github.com/angular/universal/blob/691e2fef638e0e8b5df6762684bbb76ddea79c48/integration/express-engine-ivy/server.ts#L22) function (from `@nguniversal/express-engine`). Internally it creates just [one instance of the Angular's class `CommonEngine`](https://github.com/angular/universal/blob/6005be4138d55f567c7f60ec35606ea2dd7a5a40/modules/express-engine/src/main.ts#L34) for the whole NodeJs process. Later, all requests will be handled by the same shared `CommonEngine`.

For [handling each SSR request](https://github.com/angular/universal/blob/691e2fef638e0e8b5df6762684bbb76ddea79c48/modules/express-engine/src/main.ts#L36-L40), the method [`SharedEngine.render()` is called](https://github.com/angular/universal/blob/6005be4138d55f567c7f60ec35606ea2dd7a5a40/modules/express-engine/src/main.ts#L60-L61) 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](https://github.com/angular/universal/blob/691e2fef638e0e8b5df6762684bbb76ddea79c48/modules/express-engine/src/main.ts#L62). 

## Rendering for the request

But how the result HTML is produced? 

The method [`CommonEngine.render()` calls inside the public function `renderModule()` (from `@angular/platform-server`)](https://github.com/angular/universal/blob/6005be4138d55f567c7f60ec35606ea2dd7a5a40/modules/common/engine/src/engine.ts#L104).

_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)._

#### Bootstrapping the app

The function `renderModule()` (from `@angular/platform-server`) [creates a fresh `PlatformRef`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L153), which on creation [calls `createPlatformInjector()`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L288) that [creates it's own platform's Injector](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L322-L325).

Then the app module is bootstrapped in this `PlatformRef`, by calling [`platformRef.bootstrapModule(module)`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L154).

_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._

The method `.bootstrapModule(module)`
[runs and awaits all the asynchronous APP_INITIALIZER hooks](https://github.com/angular/angular/blob/4d18ba1af78dae2ba14f45ed092a4c55335edd9c/packages/core/src/application_init.ts#L138) (by calling the method [`ApplicationInitStatus.runInitializers()`](https://github.com/angular/angular/blob/4d18ba1af78dae2ba14f45ed092a4c55335edd9c/packages/core/src/application_ref.ts#L466-L467)). Then it synchronously [bootstraps the app component] (https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L506), which causes [attaching it (and recursively it’s child components) into the DOM](https://github.com/angular/angular/blob/4d18ba1af78dae2ba14f45ed092a4c55335edd9c/packages/core/src/application_ref.ts#L1050-L1051) (however it's not a real DOM, but only a DOM representation on the server, driven by the [`DominoAdapter` class](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/domino_adapter.ts#L38)). Now, since all the `APP_INITIALIZER`s completed and all the components were rendered for the first time the app is considered as _fully bootstrapped_.

### Waiting for the app to be stable

The `Promise` returned by `platformRef.renderModule(module)` (which resolves when the app is fully bootstrapped) is passed into a [function `_render()`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L154). This function [waits for this `Promise` to resolve](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L51). And then it waits until the application _becomes stable_, by [subscribing to the observable `ApplicationRef.isStable` and awaiting the first emitted `true` value](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L64-L66). It will emit `true`, when all the pending asynchronous tasks in the app are completed (e.g. http calls to a backend API, `setTimeout`s, etc.).

_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._

### Serializing the app into a string HTML

Then [the DOM representation is serialized into a string HTML](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L93) via the [method `PlatformState.renderToString()`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/platform_state.ts#L26-L28).

_Side note: we can hook into the moment before the app is serialized, by providing the public `InjectionToken` [`BEFORE_APP_SERIALIZED`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/tokens.ts#L52-L59). For example, the [`TransferState`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-browser/src/browser/transfer_state.ts#L96) module [uses this hook](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/transfer_state.ts#L15-L20) to [embed a JSON state as a `<script>` tag into the document](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/transfer_state.ts#L29-L33) just before the app's serialization._

### Destroying the app

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](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/platform-server/src/utils.ts#L94). And this causes cascade: [destroying the app module](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L535-L545) and the [app's root `Injector`](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/render3/ng_module_ref.ts#L97), which causes [destroying all the services (also calling their `ngOnDestroy` hook)](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/di/r3_injector.ts#L176-L178) and eventually [destroying the rendered root component](https://github.com/angular/angular/blob/24950dd2a5b6a69a6afc80b8e2041f99bfd47e06/packages/core/src/application_ref.ts#L1068) and recursively destroying it's components subtree, with their relevant DOM nodes (also calling `ngOnDestroy` hook for them). 

### Garbage Collector cleaning up the memory

Now all the objects created by the app are unused (unless the app had a [_memory leak_](https://betterprogramming.pub/build-me-an-angular-app-with-memory-leaks-please-36302184e658)). And after some time, _Garbage Collector_ will clean up all those objects from the memory of the NodeJS process.

# Practical conclusions

Knowing how Angular SSR works under the hood, we can deduce a few practical conclusions:

### Hanging app instance causes a memory leak

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 🐾 ](https://dev.to/krisplatis/how-to-find-out-why-angular-ssr-hangs-track-ngzone-tasks-4nbd).

### Sharing global objects in between parallel app instances is prone to race condition

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 🏎](https://dev.to/krisplatis/don-t-use-global-static-objects-avoid-race-condition-in-ssr-angular-1iki)

### Forgetting to unsubscribe from an observable can cause the server process to crash

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.  

### Singleton services can also cause memory leaks

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 💧](https://dev.to/krisplatis/ngondestroy-in-services-unsubscribe-to-avoid-memory-leaks-in-ssr-angular-4jbj)