Ir al contenido

Hydration

Esta página aún no está disponible en tu idioma.

Hydration is the process where Angular reuses the DOM rendered on the server instead of destroying it and rendering it again.

Without hydration Angular performs a destructive bootstrap: it wipes the server HTML and re-creates every node. The user sees a visible flicker and the LCP measurement resets. With hydration Angular walks the existing DOM, attaches listeners and internal structures, and keeps the nodes.

import { provideClientHydration, withEventReplay, withIncrementalHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(
withEventReplay(),
withIncrementalHydration(),
),
],
};

Between the moment the HTML is painted and the moment Angular finishes bootstrapping, the page looks interactive but is not. Clicks in that window are lost.

withEventReplay() installs a tiny script that records user events during that gap and replays them once the component is hydrated. It is cheap and should be enabled in virtually every SSR application.

provideClientHydration(withEventReplay())

Full hydration still downloads and bootstraps the whole component tree. Incremental hydration lets you defer hydration of parts of the page until they are actually needed, while still keeping the server-rendered HTML visible.

It is expressed with @defer plus a hydrate trigger:

@defer (hydrate on viewport) {
<app-product-reviews [productId]="productId()" />
} @placeholder {
<div class="skeleton"></div>
}
TriggerHydrates when
hydrate on viewportThe block scrolls into view
hydrate on interactionThe user clicks or focuses inside the block
hydrate on hoverThe pointer enters the block
hydrate on immediateAs soon as the app is idle-free
hydrate on idleThe browser is idle
hydrate on timer(2s)After a delay
hydrate when conditionA signal/expression becomes true
hydrate neverNever — static HTML forever

For purely static regions (footers, legal text, rendered markdown) you can ship zero JavaScript:

@defer (hydrate never) {
<app-site-footer />
}

The HTML is rendered on the server and stays in the page, but the component is never instantiated on the client.

Difference between @defer and @defer (hydrate ...)

Section titled “Difference between @defer and @defer (hydrate ...)”
Classic @defer@defer (hydrate ...)
Server outputRenders the @placeholderRenders the real content
ClientDownloads chunk on triggerContent already visible, only hydrates on trigger
SEOPlaceholder gets indexedReal content gets indexed

For SSR applications you almost always want the hydrate variant.


A mismatch happens when the DOM produced on the server differs from what the client expects. Angular reports it as NG0500–NG0505 and falls back to destructive rendering for that subtree, silently killing your SSR benefit.

// Breaks hydration: Angular does not know these nodes exist
ngOnInit(): void {
const el = document.createElement('div');
this.host.nativeElement.appendChild(el);
}

Fix: move DOM work to afterNextRender, which only runs in the browser and after hydration completed.

import { afterNextRender, ElementRef, inject, Injector } from '@angular/core';
export class ChartComponent {
private readonly host = inject(ElementRef<HTMLElement>);
constructor() {
afterNextRender(() => {
renderThirdPartyChart(this.host.nativeElement);
});
}
}
<!-- Server and client produce different values -->
<p>Generated at {{ now }}</p>
<p>{{ Math.random() }}</p>

Fix: compute the value once on the server and transfer it, or render it only after hydration.

The browser silently repairs invalid HTML while parsing, so the DOM no longer matches what Angular rendered.

<!-- The browser will hoist the div out of the p -->
<p><div>Content</div></p>
<!-- Same with tables -->
<table><app-row /></table>

Fix: produce valid HTML; use <ng-container> or correct host elements (tr[appRow]).

Cause 4 — Browser-only APIs during rendering

Section titled “Cause 4 — Browser-only APIs during rendering”

window, document, localStorage, navigator do not exist on the server. See SSR Common Issues.

<div ngSkipHydration>
<app-legacy-jquery-widget />
</div>

This tells Angular to render that subtree destructively on the client.


afterRender vs afterNextRender vs lifecycle hooks

Section titled “afterRender vs afterNextRender vs lifecycle hooks”
APIRuns on serverRuns on clientFrequency
constructorYesYesOnce
ngOnInitYesYesOnce
ngAfterViewInitNoYesOnce
afterNextRenderNoYesOnce, after next render
afterRenderNoYesAfter every render

Practical rule: any code touching the DOM, measuring layout or using browser APIs goes into afterNextRender.

import { afterNextRender, afterRender } from '@angular/core';
constructor() {
// One-off initialisation (third-party libs, focus, measurements)
afterNextRender(() => this.initEditor());
// Recurring work — keep it extremely cheap
afterRender(() => this.syncScrollPosition());
}

  1. Open DevTools console and filter by NG05.
  2. Angular prints the exact node where the mismatch happened — expand it.
  3. Compare view-source: (server HTML) with the live DOM in the Elements panel.
  4. Temporarily add ngSkipHydration to narrow down the offending subtree, then remove it once fixed.
  5. In tests, assert that no hydration warnings were logged.