Library
Learning Patterns · 6 of 12
Learning Patterns
UI/UX Design

hydration strategies

Learning Patterns Lydia Hallie & Addy Osmani

Key Principle

Standard SSR decouples First Contentful Paint from Time to Interactive: the server delivers HTML fast, but the entire JS bundle must load and execute before any component responds to events. This creates the uncanny valley -- a rendered but frozen UI. Progressive hydration, selective hydration, streaming SSR, React Server Components, and islands architecture each attack this gap from a different angle. They are complementary, not competing.

Why This Matters

The hydration bottleneck is the single largest source of perceived slowness in SSR applications. Choosing the wrong strategy (or none) means users see content they cannot interact with -- degrading trust and engagement. The right strategy depends on where interactivity lives on the page.

Good Examples

Progressive Hydration defers activation of non-critical components (e.g., below-fold content) using triggers like Intersection Observer. Code-splitting defers loading; progressive hydration defers activation. Combined, they minimize both download and execution cost. Five implementation requirements: (1) SSR for all components, (2) code-splitting per component, (3) developer-defined hydration sequence, (4) no blocking input on already-hydrated chunks, (5) loading indicators for deferred chunks.

Streaming SSR replaces renderToString with renderToNodeStream (React 16+), piping HTML chunks as they render. TTFB becomes nearly constant regardless of page size. Backpressure is a hidden benefit: when the network saturates, the renderer pauses rather than buffering in memory, keeping the Node.js server responsive.

React Server Components (RSC) render to an intermediate format (not HTML), preserving client-side state on merge. All server-component dependencies contribute zero bytes to the client bundle. Early measurements show 18-29% JS bundle size reduction. RSC automates code-splitting -- server-selected Client Component imports become split points, eliminating manual React.lazy(). RSC and SSR are layered: RSC decides what to render with what data; SSR decides how to deliver it quickly.

Selective Hydration (React 18) replaces all-or-nothing hydration with per-Suspense-boundary hydration via hydrateRoot. The server flushes ready subtrees immediately; deferred subtrees stream in later with inline <script> tags that swap fallback content. Each subtree hydrates independently as its JS arrives. This also unlocks lazy-loading components during SSR, which was not possible before React 18.

Islands Architecture eliminates JS entirely for static regions. Multiple entry points replace the single-app model; each interactive island hydrates independently. Progressive hydration changes when components hydrate; islands change whether they hydrate at all. Best fit: content-heavy pages (blogs, docs, product pages) with localized interactivity. Unsuitable for interaction-heavy apps that would require thousands of islands.

Counterpoints

  • Progressive hydration fails when interactivity is unpredictable. In highly dynamic apps where any element may be the user's first interaction target, no reliable heuristic exists for hydration order.
  • Streaming SSR is not a drop-in replacement for renderToString. Frameworks that collect CSS or inject <head> elements during the render pass assume full completion before output, which streaming violates.
  • Islands architecture has a thin ecosystem. Few frameworks support it natively (Astro, Marko). Migration cost from existing frameworks is non-trivial.
  • RSC requires meta-framework integration (e.g., Next.js) for routing, data fetching, and the webpack plugin that maps Client Component imports to chunk URLs.

Key Quotes (with citations)

  • "Users may think that they can interact with the website, there are no handlers attached to the components yet." (Progressive Hydration, p. 1)
  • "Instead of generating one large HTML file containing the necessary markup for the current navigation, we can split it up into smaller chunks!" (Streaming SSR, p. 2)
  • "Code for Server Components is never delivered to the client. In many implementations of SSR using React, component code gets sent to the client via JavaScript bundles anyway." (Dan Abramov, React Server Components)
  • "React only hydrates the tree once... it needs to have fetched the JavaScript for all of the components before it's able to hydrate any of them." (Selective Hydration, p. 2)
  • "Selective hydration makes it possible to already hydrate the components that were sent to the client, even before the Comments component has been sent!" (Selective Hydration, p. 3)
  • "Each component has its hydration script in the Islands architecture that executes asynchronously, independent of any other script on the page." (Islands Architecture)

Rules of Thumb

  1. Mostly static page with isolated widgets -- use islands architecture (Astro, Marko).
  2. SSR app with heavy below-fold content -- use progressive hydration with Intersection Observer triggers.
  3. SSR app with slow data sources -- use streaming SSR + selective hydration (React 18 Suspense boundaries).
  4. Large bundle dominated by server-only logic -- use React Server Components to zero-out server dependencies from the client bundle.
  5. Combine, don't choose one. Streaming SSR accelerates delivery; progressive/selective hydration reduces activation cost; RSC reduces bundle size. They compound.

Related References

  • SSR and CSR rendering patterns (foundational)
  • Code-Splitting pattern (structural prerequisite for progressive hydration)
  • React Concurrent Mode, React.lazy(), Suspense, SuspenseList
  • Astro client:visible directive (viewport-gated island hydration)
  • Marko (eBay) streaming renderer with automatic partial hydration
  • Performance metrics: FCP, LCP, TTI, TTFB
  • Divriots.com Astro case study: FCP 1.7s to 0.5s, TTI 1.8s to 0.5s, 83% JS reduction vs. Next.js/Nuxt.js