Angular Incremental Hydration with @defer Hydrate Triggers [2026]

Link copied
Angular Incremental Hydration with @defer Hydrate Triggers [2026]

Angular Incremental Hydration with @defer Hydrate Triggers [2026]

Full hydration has a cost that grows with the page: before anything is interactive, the browser must download the JavaScript for every component on the page, run it, and walk the whole DOM. On a long product page, most of that work is for sections the user hasn't scrolled to and may never touch: reviews, a recommendations carousel, the footer. Incremental hydration lets you leave those sections as plain server-rendered HTML, with no JavaScript loaded for them, until something specific happens.

This is lesson 9.5 of the Angular Tutorial. Lesson 9.4 covered the gap between visible and interactive; this lesson shrinks that gap by hydrating less up front. It builds directly on @defer from lesson 5.6. You'll learn how hydrate triggers differ from regular @defer triggers, every trigger Angular supports, how nested and dehydrated blocks behave, and how to decide which sections are worth deferring.

Three ways a section can reach the browser #

Approach Server renders Browser downloads its JS Becomes interactive
Normal content Full content With the page During initial hydration
@defer (on viewport) (regular trigger) The @placeholder When the trigger fires After loading, replacing the placeholder
@defer (hydrate on viewport) (hydrate trigger) Full content When the trigger fires When the trigger fires, reusing the server DOM

The middle row is the problem incremental hydration solves. A regular @defer block renders only its placeholder on the server, so the server-rendered page shows a skeleton where content should be, and when the real content loads it replaces the placeholder, often with a layout shift. That's why the usual advice was to avoid @defer above the fold on SSR pages.

With a hydrate trigger, the server renders the real content. The user and crawlers see the full section immediately. In the browser, that section stays dehydrated: the HTML is there, but no component code has been downloaded or run for it. When the hydrate trigger fires, Angular loads the block's dependencies and hydrates the existing DOM in place. Nothing is replaced, so nothing shifts.

It's on by default in v22 #

Since Angular v22, provideClientHydration() enables incremental hydration automatically. The old withIncrementalHydration() feature is deprecated because it's redundant. You only need to act if you want to turn it off:

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withFetch()),
    provideClientHydration(),   // full hydration + incremental hydration + event replay
  ],
};

// To opt out (rarely needed):
// provideClientHydration(withNoIncrementalHydration())

Incremental hydration needs the same foundations as regular hydration: server-side rendering, hydration enabled, and valid, consistent markup (lesson 9.3). Event replay comes with it, which matters here, as you'll see below.

The hydrate triggers #

Hydrate triggers use the same vocabulary as regular @defer triggers, prefixed with hydrate:

Trigger Hydrates when Good for
hydrate on idle The browser is idle (optional timeout: hydrate on idle(500)) Secondary content you want ready soon, without competing with critical work
hydrate on viewport The block's content scrolls into view Below-the-fold sections: reviews, related items
hydrate on interaction The user clicks or presses a key in the block Widgets that look complete but need JS only when used
hydrate on hover The pointer moves over the block or it receives focus Menus and previews, hydrated a moment before the click
hydrate on immediate Right after the non-deferred content has rendered Content you want hydrated as early as possible, but split out of the main bundle
hydrate on timer(2s) After the given time (ms or s) Rarely the best choice; prefer idle or viewport
hydrate when expr The expression becomes truthy Hydration driven by application state
hydrate never Not on the initial load Static content that never needs JavaScript on this page

A typical product page:

<app-product-header [product]="product()" />        <!-- hydrated normally -->
<app-buy-box [product]="product()" />               <!-- hydrated normally: critical -->

@defer (hydrate on interaction) {
  <app-image-gallery [images]="product().images" />
} @placeholder {
  <div class="gallery-skeleton"></div>
}

@defer (hydrate on viewport) {
  <app-reviews [productId]="product().id" />
} @placeholder {
  <div class="reviews-skeleton"></div>
}

@defer (hydrate on idle) {
  <app-recommendations [productId]="product().id" />
} @placeholder {
  <div class="recs-skeleton"></div>
}

@defer (hydrate never) {
  <app-site-footer />
} @placeholder {
  <div class="footer-skeleton"></div>
}

On the initial server-rendered load, every section shows its full content. Only the header and buy box are hydrated up front. The gallery loads its code when the user first interacts with it, the reviews when they scroll down, the recommendations when the browser is idle, and the footer never.

Placeholders still matter #

On the initial SSR load, Angular skips the @placeholder entirely: the server rendered the real content. So why write one? Because hydrate triggers apply only to the initial page load. If the user arrives on the product page through a client-side navigation from another page, there's no server HTML to hydrate, and the block behaves like a normal @defer block with its regular triggers. Without a regular trigger, that means the default trigger, on idle, and the placeholder shows until it fires.

You can set both kinds of trigger on the same block:

@defer (on viewport; hydrate on interaction) {
  <app-store-locator />
} @placeholder {
  <div class="locator-skeleton" style="min-height: 420px"></div>
}

Read it as two rules: on an SSR load, keep the server content dehydrated until the user interacts with it; on a client-side navigation, load it when the placeholder scrolls into view. Size the placeholder to match the real content so client-side navigations don't shift either.

Event replay and dehydrated blocks #

What happens when the user clicks a button inside a dehydrated block? Event replay (lesson 9.4) captures the click. For a hydrate on interaction block, the click itself is the trigger: Angular loads the block's code, hydrates it, and then replays the click to the now-attached handler. The user's first click works; it's just slightly delayed by the download.

For other triggers, such as hydrate on viewport or hydrate never, a click on content that isn't hydrated yet is queued and replayed once that block hydrates. Keep this in mind when choosing triggers: a block with buttons the user is likely to press right away should be hydrated early (normal content, hydrate on immediate or hydrate on hover), not on a trigger that may never fire.

Nesting and hydration order #

Blocks can be nested, and hydration always proceeds from the outside in: a component can't be hydrated before its parents. If a nested block's trigger fires first, Angular hydrates the enclosing dehydrated blocks first, then the nested one.

@defer (hydrate on viewport) {
  <app-reviews-section>
    @defer (hydrate on interaction) {
      <app-review-composer />
    } @placeholder {
      <div class="composer-skeleton"></div>
    }
  </app-reviews-section>
} @placeholder {
  <div class="reviews-skeleton"></div>
}

Interacting with the composer before the section has scrolled into view hydrates the section first, then the composer.

Two nesting rules are easy to miss:

  • hydrate when only works on the top-most dehydrated block. Its expression is evaluated in the parent component, which must already be hydrated to evaluate anything. On a nested dehydrated block, the condition has no hydrated parent to run in.
  • hydrate never covers the whole subtree on the initial load. Nested blocks inside it don't hydrate, whatever their own triggers say. After a client-side navigation, regular triggers apply as usual; it isn't permanent.

When incremental hydration wins #

Every deferred block has some overhead: an extra chunk to request and a small delay when the trigger fires. Use it where the trade-off is clearly positive:

Section Recommendation Why
Header, primary navigation, main call to action Hydrate normally Users interact with them first; delay would hurt INP
Heavy interactive widget near the top (gallery, configurator) hydrate on interaction or hydrate on hover Content visible immediately; code only when used
Below-the-fold sections with some interactivity hydrate on viewport Most users never reach some of them
Content needed soon but not critical hydrate on idle Keeps the main bundle small without long delays
Static content rendered by components (footer, legal text, article body) hydrate never No code shipped for it at all on SSR loads
Small, cheap components Hydrate normally Splitting them costs more than it saves

The usual @defer rules apply: the components inside the block must be standalone and must not be referenced outside the @defer block in the same file (including in viewChild queries), or they are loaded eagerly. Check with the bundle analysis from lesson 8.5 that each block really produces its own chunk.

Measure the result with the metrics from lesson 8.6. Expect lower Total Blocking Time and better INP on the initial load, unchanged LCP (the content was already server-rendered), and no new layout shift.

Gotchas #

Symptom Cause Fix
Deferred section shows a skeleton on the server-rendered page Block uses only regular triggers (on viewport), which render the placeholder on the server Add a hydrate trigger (hydrate on viewport)
Block's code is in the main bundle anyway The deferred component is referenced outside the block in the same file, or in a viewChild query Remove those references so it can be split
Skeleton flashes after client-side navigation Hydrate triggers only apply to the initial load Add a regular trigger and size the placeholder
hydrate when never fires The block is nested inside another dehydrated block Move the condition to the outer block, or use another trigger
Button in the footer does nothing until much later Footer marked hydrate never contains interactive elements Use hydrate on interaction for blocks with controls
NG0500-family errors inside a deferred block Same mismatch causes as full hydration Fix the markup or DOM manipulation (lesson 9.3)

What's next #

Lesson 9.6 deals with data: how the HttpClient transfer cache stops the browser from repeating the requests the server already made, how to configure what it caches, how to transfer your own values with TransferState, and how resource() and httpResource() fit in.

Try it yourself #

Pick your longest server-rendered page and list its sections from top to bottom. Assign each one a strategy from the table above, wrap the two heaviest below-the-fold sections in @defer (hydrate on viewport) with sized placeholders, and compare Total Blocking Time and the main bundle size before and after.

YouOur article pages are server-rendered. The comments section is heavy, and when we wrapped it in @defer (on viewport) the SSR page showed a grey skeleton instead of the comments. How do we keep the comments in the HTML but not load their code up front?
Claude · used search_documentationAdd a hydrate trigger. Regular @defer triggers are ignored on the server, so it renders the @placeholder, which is your grey skeleton. Change the block to @defer (on viewport; hydrate on viewport) { <app-comments /> } @placeholder { … }. On an SSR load the server now renders the full comments, and the browser keeps them dehydrated, with no comments code downloaded, until the section scrolls into view. The regular on viewport still covers client-side navigations, where there’s no server HTML to hydrate. In v22 you don’t need withIncrementalHydration(); provideClientHydration() already enables it. Make sure Comments isn’t referenced outside the block in the article component, or it won’t be split into its own chunk.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied Angular When Angular is launched ? February 8, 2024 · 1 min read When was Angular launched? # Angu…

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

Leave a Comment

Your email stays private. Required fields are marked *

Leave a Comment

Your email stays private. Required fields are marked *