Setting Up Angular Universal with Angular 14
First, make sure your Angular 14 project includes Angular Universal support. Use the Angular CLI to add it:
ng add @nguniversal/express-engine
This command sets up everything you need, including a server module, Express server, and TypeScript configuration.
server.ts
- Basic Express Setup
import 'zone.js/node';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
const app = express();
app.engine('html', ngExpressEngine({
bootstrap: AppServerModule,
}));
app.set('view engine', 'html');
app.set('views', join(__dirname, 'browser'));
app.get('*', (req, res) => {
res.render('index', { req });
});
app.listen(4000, () => {
console.log(`Node server listening on http://localhost:4000`);
});
Under the Hood: SSR Lifecycle Explained
When a request comes in, the Universal engine processes it using the following steps:
ngExpressEngine()
wraps Angular’sCommonEngine
.- Each request calls
CommonEngine.render()
. render()
internally calls Angular’srenderModule()
from@angular/platform-server
.
SSR Flow in Angular Universal
renderModule(AppServerModule, {
document: indexHtml,
url: req.url
}).then(html => res.send(html));
- A new
PlatformRef
is created. - Angular bootstraps the app module.
- It waits for the app to become stable (i.e. all async tasks complete).
- The DOM (powered by the Domino library) is serialized into HTML and sent back.
Application Stability Before Rendering
Angular checks when the app is stable using ApplicationRef.isStable
. Only after all pending async tasks are done does it continue rendering:
appRef.isStable
.pipe(first(isStable => isStable))
.subscribe(() => serializeDocument());
This guarantees that HTTP-loaded data is rendered and visible in the HTML response.
Cleanup: Destroying the App
After rendering is complete:
PlatformRef.destroy()
is called.- App module, root component, and all services are torn down.
- All
ngOnDestroy()
lifecycle hooks are invoked.
This frees memory and ensures no SSR state leaks into other requests.
Gotchas and Best Practices
Pending Async Tasks → Memory Leaks
If a request never completes (e.g. hanging HTTP call), the app never stabilizes. This prevents rendering and causes memory leaks.
Shared Global State → Race Conditions
Avoid using global variables or static state. SSR renders apps in parallel, and shared mutable state can lead to race conditions.
Missing Unsubscribe → Memory Waste
Make sure to clean up all RxJS subscriptions:
ngOnDestroy() {
this.subscription?.unsubscribe();
}
Even services should handle teardown logic to avoid server memory bloat.
Summary
- Use
@nguniversal/express-engine
for Angular 14 SSR. - Rendering uses
renderModule()
from@angular/platform-server
. - Angular waits until app stability before HTML serialization.
- Always clean up subscriptions and avoid shared state.
SSR in Angular 14 with Universal gives you faster first-paint times and better SEO. But it requires precise control over lifecycle management and asynchronous logic.
Master these fundamentals, and your server-rendered Angular apps will scale beautifully.