SSR, hydration and render modes

Angular · Expert · 16 min

Server rendering improves first paint and SEO, but introduces platform-specific bugs that need expert handling.

Angular's SSR support lives in @angular/ssr. Create an app with ng new --ssr or add it with ng add @angular/ssr.

Render modes per route are configured in app.routes.server.ts with ServerRoute entries: RenderMode.Prerender (static HTML at build time, with getPrerenderParams for dynamic segments), RenderMode.Server (rendered per request) or RenderMode.Client (browser only). They are registered with provideServerRendering(withRoutes(serverRoutes)) in the server config.

Hydration reuses the server-rendered DOM instead of re-creating it. It is enabled with provideClientHydration(); add withEventReplay() to capture clicks that happen before the code loads and replay them, and withIncrementalHydration() to keep parts of the page dehydrated until needed: @defer (hydrate on viewport) or hydrate on interaction renders the content on the server but only downloads and hydrates its code when triggered.

HttpClient requests made during SSR are cached into the HTML and reused on the client (the HTTP transfer cache), so data isn't fetched twice.

Pitfalls:

  • No window, document layout or localStorage on the server. Use afterNextRender or check isPlatformBrowser(inject(PLATFORM_ID)), and inject DOCUMENT instead of the global.
  • Hydration requires the DOM to match: direct DOM manipulation or invalid HTML nesting causes errors (ngSkipHydration is an escape hatch).
  • Long-running timers or never-completing work keep the server waiting for the app to become stable.
  • Server-side, services are per request, but module-level variables are shared between users.

Example

// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },              // static at build time
  { path: 'products/:id', renderMode: RenderMode.Prerender,
    async getPrerenderParams() { return [{ id: '1' }, { id: '2' }]; } },
  { path: 'account/**', renderMode: RenderMode.Client },       // browser only
  { path: '**', renderMode: RenderMode.Server },               // per request
];
// app.config.server.ts:  provideServerRendering(withRoutes(serverRoutes))
// app.config.ts:         provideClientHydration(withEventReplay(), withIncrementalHydration())
// theme.ts - browser APIs used safely
import { Component, afterNextRender, signal } from '@angular/core';
@Component({
  selector: 'app-theme',
  template: `<p>Theme: {{ theme() }}</p>
             @defer (hydrate on viewport) { <p>Hydrated lazily</p> }`,
})
export class Theme {
  protected theme = signal('light');           // same on server and first client render
  constructor() {
    afterNextRender(() => this.theme.set(localStorage.getItem('theme') ?? 'light'));
  }
}

Remember

  • Per-route RenderMode: Prerender, Server or Client
  • provideClientHydration() with event replay and incremental hydration (@defer (hydrate on ...))
  • Browser-only code goes in afterNextRender or behind isPlatformBrowser
  • HttpClient responses are transferred from server to client automatically

Watch out for

  • Reading localStorage during rendering, which crashes on the server or causes a hydration mismatch.

Check yourself

  1. What does @defer (hydrate on viewport) do in an SSR app with incremental hydration?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/ssr