Skip to content
Angular ng ssr 4 min read

Hydration

When you render an Angular app on the server, the browser receives fully-formed HTML that paints instantly. But that markup is inert until the JavaScript bundle loads and Angular takes over. Hydration is the process that wires the running application to the existing DOM. Modern Angular performs non-destructive hydration: instead of throwing away the server HTML and re-rendering from scratch, it reuses the existing nodes, attaches event listeners, and resumes the application in place. The result is less flicker, fewer layout shifts, and a meaningfully faster First Input Delay.

Why non-destructive hydration matters

Before hydration was available, an SSR Angular app would destroy the server-rendered DOM on bootstrap and rebuild the entire tree on the client. Users saw a flash of content, then a blank moment, then the re-rendered page — a jarring “double paint.” It also wasted CPU re-creating nodes that already existed and discarded the streaming performance benefits of SSR.

Non-destructive hydration walks the existing DOM in lockstep with the component tree, claiming each server-rendered element rather than recreating it. Angular only creates new DOM where the server and client trees legitimately differ.

AspectDestructive (legacy)Non-destructive hydration
Server DOMDiscarded and replacedReused in place
Visible flickerYes (“double paint”)No
Re-render costFull tree rebuildListeners attached only
Layout shiftCommonMinimal
Streaming benefitLost on bootstrapPreserved

Enabling hydration

Hydration is opt-in through a single provider added to your application config. If you scaffolded SSR with ng add @angular/ssr, the provider may already be present — otherwise add it to app.config.ts.

// src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
  provideClientHydration,
  withEventReplay,
} from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    // Enables non-destructive hydration for the whole app
    provideClientHydration(
      withEventReplay(), // optional: replay clicks that fired before hydration
    ),
  ],
};

That single call activates hydration globally. You do not need to annotate individual components — Angular reconciles the entire tree automatically.

Tip: provideClientHydration() is a no-op in a browser-only (non-SSR) app, so it is safe to leave in a shared config. It only has an effect when the app was rendered on the server.

Verifying it works

The easiest signal is the browser console. With hydration active, Angular logs a summary in development mode, and you should not see your top-level component’s DOM blink or rebuild.

ng serve --ssr

Output:

Angular hydrated 1 component(s) and 48 node(s), 0 component(s) were skipped.
Angular is running in development mode.

You can also confirm by inspecting the network waterfall: the initial HTML response already contains your rendered markup, and once JS loads, the DOM nodes keep their original identity rather than being replaced.

How it stays in sync

Hydration relies on the server and client producing the same DOM structure. Angular serializes a small amount of state into the page (inside an ngh attribute payload) so the client knows how to map nodes to components and where text boundaries fall. During bootstrap Angular reads this map and claims nodes top-down.

If the client tree diverges from what the server produced, Angular reports a hydration mismatch and falls back to re-rendering the affected sub-tree to stay correct.

NG0500: During hydration Angular expected <div> but found <span> ...

Common causes of mismatches:

  • Direct DOM manipulation (document.querySelector(...), innerHTML) that the server never ran.
  • Browser-only APIs (window, localStorage) producing different markup than the server.
  • Invalid HTML nesting that the browser silently “fixes” (e.g. a <div> inside a <p>).
  • Conditionals based on typeof window !== 'undefined' that change the rendered output.

Working with control flow and signals

Hydration composes cleanly with modern Angular. The new control-flow blocks render identically on both platforms as long as the underlying data is the same, so prefer them over ad-hoc DOM work.

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-greeting',
  standalone: true,
  template: `
    @if (user(); as u) {
      <p>Welcome back, {{ u.name }}.</p>
    } @else {
      <p>Welcome, guest.</p>
    }
  `,
})
export class GreetingComponent {
  // Resolved on the server, transferred to the client — same output, no mismatch
  readonly user = signal<{ name: string } | null>({ name: 'Ada' });
}

To carry server-fetched data across so the client doesn’t re-fetch (and re-render differently), use provideHttpClient(withFetch()) together with TransferState, which HttpClient populates automatically when hydration is enabled.

Skipping hydration for a subtree

Some third-party widgets manage their own DOM and break under hydration. Mark their container with ngSkipHydration to tell Angular to leave that subtree alone and let the client render it conventionally.

<!-- Angular will not hydrate this element or its children -->
<div ngSkipHydration>
  <legacy-chart-widget></legacy-chart-widget>
</div>

Use this sparingly — every skipped subtree loses the benefits of hydration for that region.

Best practices

  • Add provideClientHydration() once in app.config.ts and let it cover the whole app rather than reaching for per-component workarounds.
  • Render UI from component state (signals, inputs, control flow) instead of imperative DOM mutation so server and client output match.
  • Enable withFetch() on HttpClient so TransferState deduplicates data fetching and prevents content-shift mismatches.
  • Reserve ngSkipHydration for genuinely DOM-owning third-party widgets, not as a band-aid for fixable mismatches.
  • Watch the dev console for NG0500 mismatch warnings and treat each one as a bug to fix, not noise to ignore.
  • Validate your HTML nesting — illegal nesting is a frequent and easily-overlooked source of mismatches.
Last updated June 14, 2026
Was this helpful?