Skip to main content

6 min read #angular #hydration #ssr #performance #frontend

Incremental hydration: ship the HTML, hydrate on hover

Full hydration replays your whole app the moment it loads, even the footer nobody scrolls to. Incremental hydration with @defer (hydrate on ...) renders everything on the server but wires up the JavaScript only when each block is about to be used.

Server-side rendering gets you a fast first paint: the browser receives real HTML and shows it immediately. Then hydration happens — Angular boots on the client, walks the entire component tree, and wires up every event listener so the page becomes interactive. The problem is the word "entire." Your footer, your three-tabs-deep settings panel, the comment section nobody scrolls to — all of it gets hydrated up front, competing for the main thread while the user is staring at the hero. That's the gap between "I can see it" and "I can click it," and it's mostly wasted work.

Incremental hydration closes that gap by making hydration lazy, per block. You opt in once:

bootstrapApplication(App, {
  providers: [provideClientHydration(withIncrementalHydration())],
});

and then mark the regions that can wait, using the @defer syntax you already know — but with a hydrate trigger:

@defer (hydrate on viewport) {
  <app-comments [postId]="postId" />
}

The comments render on the server as normal HTML — visible, indexable, no layout shift — but their JavaScript doesn't download or hydrate until the block scrolls into view. The main thread stays free for the part of the page the user is actually looking at.

This is not the same as a plain @defer

Worth being precise, because the syntax is shared and the behaviors are opposite. A regular @defer (on viewport) renders nothing on the server — you get a @placeholder until the trigger fires, then the real content loads in. Great for cutting the initial bundle, but the deferred content isn't in the server HTML, so it's invisible to a crawler and pops in late for the user.

@defer (hydrate on ...) is the inverse. The content is fully rendered on the server — it's there in the HTML from the first byte. What's deferred is only the hydration: the download and execution of its JavaScript. So you keep the SEO and the instant paint of SSR, and you also get the deferred-work win. It's the best of both, which is why it needed a new trigger keyword rather than reusing the old behavior.

hydrate never is a static island

The trigger I find most quietly powerful is hydrate never. Plenty of server-rendered content is genuinely static — a rendered markdown article, a pricing table, a marketing block. It has no interactivity to wire up, yet full hydration still spends cycles proving that to itself.

@defer (hydrate never) {
  <app-article-body [markdown]="content" />
}

hydrate never says: this is finished, ship it as HTML and send no JavaScript for it, ever. It becomes a true static island in an otherwise interactive app. For a content-heavy page, that's a real cut to the JavaScript the client has to parse — the bytes for that subtree simply never arrive.

The clicks aren't lost — event replay catches them

The obvious worry: if a button's JavaScript hasn't hydrated yet and the user clicks it, does the click vanish? This is exactly what event replay solves, and it's why incremental hydration leans on it. With withEventReplay() (on by default in current setups), events that land on not-yet-hydrated content are captured and queued. The trigger fires, the block hydrates, and the queued events replay against the now-live listeners. From the user's side the click just worked, maybe a hair late. Without event replay, that early click really would be dropped — so treat the two features as a pair.

It's a different tool from afterNextRender

Since both live under the SSR umbrella, it's worth drawing the line. afterNextRender answers "my code touches document and crashes on the server — when is it safe to run browser-only logic?" Incremental hydration answers a different question: "the whole page works, but when should each piece become interactive?" One is about avoiding server-side errors; the other is about scheduling client-side interactivity. You'll often use both on the same app for entirely separate reasons.

The decision rule

Reach for hydrate on viewport or hydrate on interaction for heavy interactive widgets that sit below the fold or get used rarely — comment threads, data grids in a collapsed panel, a map at the bottom of the page. Use hydrate never for server-rendered content with no interactivity at all. Leave the above-the-fold, immediately-interactive parts — your nav, your primary call to action — hydrating normally, because deferring those just adds latency to the thing the user reaches for first. The whole point is to stop spending the main thread on interactivity nobody has asked for yet.