Skip to content

PATTERN Cited by 2 sources

Progressive hydration (per-Renderer opt-in)

Problem

A server-rendered React page paints quickly but is not interactive until every component in the tree has been hydrated — hydrate() walks the whole tree and attaches event handlers in one pass. For a page assembled from many Renderers, this gap between visible and interactive can be several seconds: the user sees content, taps a button, and nothing happens until the hydrate call reaches that subtree.

The naïve fix — "ship less JavaScript" — hits a ceiling because above-the-fold components genuinely need to be interactive, and the below-the-fold components extend the hydration time even though nothing visible is happening there.

Solution

Make hydration per-component opt-in, ordered by visibility and interaction priority, rather than one global pass. A framework-level marker on a component says "hydrate this as soon as possible, independently of where it sits in the tree." The rest of the tree hydrates lazily, or when scrolled into viewport, or on first interaction.

Applied to the Rendering Engine's Renderer abstraction (Source: sources/2021-09-08-zalando-micro-frontends-from-fragments-to-renderers-part-2):

"Progressive Hydration: we can mark specific renderers to be hydrated early, i.e. kicking off their React hydration as fast as possible on the client-side, and thus making its content interactive before its parent renderer."

Each Renderer is a natural hydration boundary — it owns one Entity's worth of UI, has its own GraphQL data dependencies, and is independently shippable. Marking a Renderer as hydrate-early lifts that one subtree out of the whole-tree hydrate and attaches handlers first.

Mechanics

The framework needs four pieces to make this work:

  1. Per-Renderer metadata declaring hydration priority (immediate / viewport / interaction / idle).
  2. Per-subtree hydrate call, not whole-tree — implemented on React 18+ naturally via Suspense boundaries; in pre-React-18 (Zalando's 2021-era implementation) it had to be hand-rolled.
  3. Code splitting so early-hydrate Renderers' code can arrive and execute before lower-priority chunks do.
  4. Data fetching hoisted to the server, so the early-hydrate Renderer doesn't stall the client on useEffect-time fetches.

In Zalando's case pre-React-18 this was built entirely in-house — the Rendering Engine shipped its own streaming + partial-hydration. The 2023 React 18 migration (Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react) delegated the streaming + progressive-hydration implementation to upstream React (via renderToPipeableStream + hydrateRoot + <Suspense>), with each Renderer → one <Suspense> boundary as the architectural mapping.

Why per-Renderer grain is the right boundary

The Renderer abstraction (see patterns/entity-to-renderer-mapping) already provides what progressive hydration needs:

  • Self-contained scope — a Renderer owns one Entity's data and UI; no cross-Renderer mutable state that would be broken by partial hydration.
  • Declared data dependencies — via withQueries, the framework knows the data is resolvable server-side without waiting for client-side effects.
  • Natural code-split boundary — each Renderer is a separately loadable module.

Finer-grained hydration (per-component inside a Renderer) isn't needed; coarser-grained (whole-page) is the problem being solved.

When to reach for it

  • The page has clear above-the-fold vs below-the-fold priority differences.
  • The codebase already has a Renderer-like abstraction — components with declared data dependencies and independent lifecycle.
  • Initial interactivity latency (INP / FID) is a measured concern and whole-tree hydration is the bottleneck.
  • Streaming SSR is available or planned (the natural delivery shape for progressive hydration).

When not to

  • The page has tight cross-component state coupling (shared mutable stores where early-hydrating one part breaks invariants of the late-hydrating rest).
  • All components are roughly equally time-sensitive (the gain from staggering is small; the complexity isn't).
  • The framework doesn't expose a per-boundary hydrate API (pre-React-18 React's whole-tree hydrate; hand-rolled partial hydrate is possible but expensive to maintain).

Measured impact (Zalando 2023 React 18 migration)

After swapping the narrow API pair (renderToPipeableStream + hydrateRoot) — with the Renderer-to-<Suspense> mapping but no other concurrent-feature adoption — Zalando reported across Fashion Store pages (Source: sources/2023-07-10-zalando-rendering-engine-tales-road-to-concurrent-react):

  • INP −5.69 %
  • FID −8.81 %
  • LCP −2.43 %
  • FCP −0.23 %
  • Bounce rate −0.24 %

Catalog — the most Renderer-dense page — showed the largest interactive-latency gains (INP −6.76 %, FID −17.11 %), consistent with progressive hydration reducing time-to-interactive for the longest Renderer trees.

Seen in

Last updated · 550 distilled / 1,221 read