Angular PrimerAngular 22 · 2022 → 2026

SSR & hydration

Rendering the page on the server (or at build time) so the HTML arrives with content in it, then letting the browser take over the same DOM instead of re-rendering it.

Two different things people call SSR

It is worth separating them, because the choice matters more than the implementation:

  • Prerendering (SSG) — the HTML is produced at build time for a known set of routes. Every visitor gets a static file, and there is no server rendering per request. This is what a documentation site, a marketing page or a blog wants.
  • Server-side rendering (SSR) — the HTML is produced per request on a Node server, which can personalise it, read cookies, or render data that changes every minute.

Angular's @angular/ssr supports both from one codebase, and a route can be either: outputMode: "static" prerenders everything, outputMode: "server" renders on demand, and a per-route config can mix them.

2026 — per-route render modes
// app.routes.server.ts
export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'topics/:slug', renderMode: RenderMode.Prerender, async getPrerenderParams() { /* ... */ } },
  { path: 'dashboard', renderMode: RenderMode.Server },       // per request
  { path: 'settings', renderMode: RenderMode.Client },        // browser only
];

This site is a worked example. It prerenders every route at build time: npm run build writes a real index.html per topic, with the article text, the headings and their anchors already in the file. View the source of any page here and the prose is there before a byte of JavaScript runs.

Adding it

ng add @angular/ssr

That adds a server entry point, a server config file, and provideClientHydration() to app.config.ts. With outputMode: static, the build prerenders the routes you list.

2026 — angular.json
"outputMode": "static",
"prerender": { "routesFile": "prerender-routes.txt", "discoverRoutes": false }

A routesFile is often better than discoverRoutes: true because it makes the set of prerendered pages explicit and reviewable — this site generates it from the topic list, so adding a topic adds its prerendered route.

Hydration: taking over the DOM

Hydration is the part that makes SSR feel fast rather than merely look fast. Without it, the browser receives rendered HTML and then Angular throws it away and re-renders everything — the "flicker" that made early SSR feel worse than a plain SPA. With provideClientHydration(), Angular reuses the server's DOM and attaches behaviour to it.

2026 — app.config.ts
providers: [
  provideClientHydration(withEventReplay()),
]

Two add-ons worth knowing:

  • Event replay — records the user's clicks and keystrokes that happen before hydration finishes, and replays them into Angular once it is live. Without it, a click during those first few hundred milliseconds is lost.
  • Incremental hydration — hydration driven by @defer: instead of hydrating the whole page at once, the parts inside @defer are hydrated when their trigger fires. It is the feature that makes a heavy page cheap on a slow device.

Code that must not run on the server

During prerendering there is no window, no document, no localStorage. This is the single most common source of "it works locally, the build fails" bugs:

2026 — guarding browser-only code
export class Theme {
  private readonly storage = inject(DOCUMENT).defaultView?.localStorage;   // may be undefined

  constructor() {
    afterNextRender(() => {
      this.applyToDocument();          // runs only in the browser
    });
  }
}

The tools, in order of preference:

  1. afterNextRender() — run this only in the browser, after the first render. Best for DOM measurement and third-party widgets.
  2. isPlatformBrowser(inject(PLATFORM_ID)) — a straightforward branch when the work is not render-related.
  3. inject(DOCUMENT) instead of document — the Angular-provided document, which is safe on the server and is what makes SSR testable.

Writing to a signal from a timer, a localStorage read, or a third-party callback all need the same care: they cannot be your only source of a value during prerender.

If your prerendered HTML differs from what the client renders, hydration throws a mismatch and Angular recovers by throwing away the DOM and re-rendering — which is exactly the flicker you added SSR to avoid. The usual causes: reading Date.now() or Math.random() in a template, reading localStorage during render, and rendering different markup for logged-in users on the server. Make the first render deterministic, and move anything environment-dependent into afterNextRender.

Transfer state

When the server already fetched data to render the page, you do not want the browser to fetch it again. Transfer state is how the server's values are serialised into the HTML and handed to the client, so resource() and HTTP calls made during render do not repeat on hydration. The ng-state script tag in a prerendered page's HTML is that payload.

What to take away

SSR stopped being an add-on for SEO and became a build mode with a per-route choice: prerender what is static, render per request what is personal, and hydrate so the browser inherits work already done. It also makes you strict about where your data comes from — which is a good discipline for the client anyway. Next: Un-learn & upgrade, which is where the porting advice lives.