---
title: Progressive Hydration
description: A strategy that hydrates a server-rendered page in pieces — prioritizing visible or interactive sections — instead of hydrating the entire component tree at once.
tokens: ~1063
---

# Progressive Hydration

After [SSR](/docs/web/rendering/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:

```typescript
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](/docs/web/rendering/streaming) 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.

```typescript
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 |