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
| Framework | Mechanism |
|---|---|
| Astro | Islands architecture — only interactive components ship JS at all |
| Qwik | Resumability — 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
| Strength | Weakness |
|---|---|
| Lower TTI — critical sections become interactive sooner | All component JS still ships; execution is deferred, not eliminated |
| Better perceived performance — the part the user cares about is ready first | Requires <Suspense> boundaries or framework-specific APIs |