Skip to content
Angular ng ssr 4 min read

Incremental Hydration

Full hydration walks the entire server-rendered DOM and attaches the framework to every component as soon as the page loads, which means you pay the JavaScript cost for parts of the page the user may never reach. Incremental hydration, stable since Angular 19, lets you defer that work per-region: a block renders on the server, ships as static HTML, and only hydrates (downloading its code and wiring up its event listeners) when a trigger you specify fires. The result is a smaller initial bundle, faster Time to Interactive, and content that is visible and SEO-friendly from the first paint.

How incremental hydration works

Incremental hydration is built on top of the @defer block. With ordinary client-side rendering, @defer shows a @placeholder until a trigger loads the deferred content. With incremental hydration the deferred content is fully rendered on the server and sent as real HTML — there is no empty placeholder gap. Angular leaves that DOM dormant (no event listeners, no change detection) until a hydrate trigger fires, at which point it fetches the component’s code and hydrates the existing DOM in place rather than re-rendering it.

To opt in, you pass withIncrementalHydration() to the client hydration provider. It implies withEventReplay() automatically, so interactions that happen before a region hydrates are queued and replayed once it becomes interactive.

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import {
  provideClientHydration,
  withIncrementalHydration,
} from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(withIncrementalHydration()),
  ],
};

Hydrate triggers

Inside a @defer block you add an @defer (hydrate ...) clause. The hydrate keyword distinguishes a hydration trigger from a regular load trigger. The available triggers mirror the familiar @defer ones, plus a special never.

TriggerHydrates when…
hydrate on idlethe browser is idle (requestIdleCallback); the default-feeling choice
hydrate on viewportthe block scrolls into the viewport
hydrate on interactionthe user clicks, focuses, or otherwise interacts with the block
hydrate on hoverthe pointer hovers over the block
hydrate on immediateas soon as the client finishes its initial render
hydrate on timer(duration)a timer elapses, e.g. timer(5s)
hydrate when conditiona boolean expression becomes true
hydrate nevernever — the block stays static, inert HTML forever
<!-- product-page.component.html -->
@defer (hydrate on viewport) {
  <app-related-products [categoryId]="categoryId()" />
} @placeholder {
  <div class="skeleton">Loading recommendations…</div>
}

Because the content is server-rendered, the @placeholder is only used as a fallback during pure client-side navigation; on the initial SSR load the user sees the real component immediately.

Combining triggers and never

You can list several triggers and the block hydrates when the first one fires. Use hydrate never for content that should remain on the page for SEO and layout but never needs interactivity — for example a static legal footer.

@defer (hydrate on interaction; hydrate on timer(8s)) {
  <app-comment-thread [postId]="postId()" />
} @placeholder {
  <p>Comments</p>
}

@defer (hydrate never) {
  <app-license-notice />
}

Tip: hydrate never keeps the JavaScript for that component out of the user’s session entirely. It is the most aggressive saving available — reach for it on truly static regions.

Nested defer blocks

When you nest @defer (hydrate ...) blocks, hydrating an outer block does not automatically hydrate the inner ones. Each block keeps its own trigger. This lets you hydrate a wrapping section on viewport while leaving an expensive child inside it on interaction, hydrating the heavy widget only if the user actually engages with it.

@defer (hydrate on viewport) {
  <section class="dashboard">
    <h2>Activity</h2>

    @defer (hydrate on interaction) {
      <app-live-chart [data]="metrics()" />
    } @placeholder {
      <img src="/chart-snapshot.png" alt="Activity chart" />
    }
  </section>
}

Verifying it works

Build and serve with SSR, then inspect the network panel. Code for a deferred region should only be requested when its trigger fires. Angular also logs hydration mismatches to the console in dev mode.

ng build && node dist/your-app/server/server.mjs

Output:

Node Express server listening on http://localhost:4000
✔ Server-side rendered: /product/42
  related-products-chunk.js  ── not requested until viewport reached

Scrolling the app-related-products block into view triggers the lazy chunk download and the region becomes interactive, with any clicks made during the gap replayed via event replay.

Best practices

  • Wrap regions in @defer (hydrate ...) only when they are below the fold or non-critical — above-the-fold, interaction-heavy content is better fully hydrated.
  • Prefer hydrate on viewport for long pages and hydrate on interaction for widgets the user may never touch (modals, tabs, comment threads).
  • Use hydrate never for purely presentational, SEO-relevant content to eliminate its JavaScript completely.
  • Keep @placeholder blocks lightweight and visually close to the real content, since they only appear during client-side navigation.
  • Ensure deferred components avoid direct window/document access at construction time so server rendering stays clean.
  • Test with throttled network and CPU to confirm the deferred chunks load on the triggers you expect and not eagerly.
Last updated June 14, 2026
Was this helpful?