Last updated: Apr 29, 2026

Progressive Hydration

After SSR, the browser has a fully painted page but no interactivity. Hydration attaches event listeners and restores component state so the static HTML becomes a live application. Traditional hydration walks the entire component tree in one pass. Progressive hydration breaks this work into smaller pieces and schedules them by priority — viewport-visible and interactive sections first, off-screen and static sections later (often via requestIdleCallback).

┌───────────────────────────────────┐
│         Server-rendered HTML       │
├──────────┬──────────┬─────────────┤
│  Header  │  Search  │  Hero       │  ← hydrated immediately (visible, interactive)
├──────────┴──────────┴─────────────┤
│  Product grid                     │  ← hydrated on viewport entry
├───────────────────────────────────┤
│  Reviews (collapsed)              │  ← hydrated on interaction (click to expand)
├───────────────────────────────────┤
│  Footer                           │  ← hydrated during idle time
└───────────────────────────────────┘

Intersection Observer Pattern

A common implementation wraps deferred components and delays hydration until the element enters the viewport:

import { useState, useEffect, useRef, type ReactNode } from "react";

function HydrateOnVisible({ children }: { children: ReactNode }) {
  const ref = useRef<HTMLDivElement>(null);
  const [hydrated, setHydrated] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setHydrated(true);
          observer.disconnect();
        }
      },
      { rootMargin: "200px" },
    );
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return <div ref={ref}>{hydrated ? children : null}</div>;
}

The server-rendered HTML stays visible regardless — children controls whether the client-side component tree mounts (and thus whether event listeners attach), not whether the HTML is shown.

Framework Approaches

FrameworkMechanism
AstroIslands architecture — only interactive components ship JS at all
QwikResumability — no hydration step; event handlers are lazy-loaded on use
Next.js<Suspense> + React 18 selective hydration (see below)
Angular@defer blocks with trigger conditions (on viewport, on interaction)

Selective Hydration

Selective hydration is React 18’s built-in approach to progressive hydration. When using streaming SSR with renderToPipeableStream, React can hydrate <Suspense> boundaries independently and in any order.

The server streams HTML for the full page. On the client, React hydrates each <Suspense> boundary as its HTML and JS arrive. If the user interacts with a component that has not been hydrated yet, React prioritizes that boundary — it pauses whatever it is currently hydrating and switches to the one the user clicked.

import { lazy, Suspense } from "react";

const Comments = lazy(() => import("./Comments"));
const Sidebar = lazy(() => import("./Sidebar"));

function Page() {
  return (
    <main>
      <article>{/* hydrated with the shell */}</article>
      <Suspense fallback={<SidebarSkeleton />}>
        <Sidebar />
      </Suspense>
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </main>
  );
}

Each <Suspense> boundary hydrates independently — one slow boundary does not block others. Unlike manual progressive hydration with Intersection Observer, selective hydration is automatic once the app uses <Suspense> and streaming.

Trade-offs

StrengthWeakness
Lower TTI — critical sections become interactive soonerAll component JS still ships; execution is deferred, not eliminated
Better perceived performance — the part the user cares about is ready firstRequires <Suspense> boundaries or framework-specific APIs