Learning Patterns Deep‑Dive – Pattern 45
Based on “Learning Patterns” by Lydia Hallie & Addy Osmani
Table of Contents
-
Progressive Hydration in React: Taming the Hydration Monster
- Table of Contents
- The Hydration Problem
- Why Traditional Hydration Breaks Down
- Meet Progressive Hydration
- How it Works
- Implementation Walk‑through
- Step 1 – Identify Hydration Boundaries
- Step 2 – Lazy‑load & Hydrate on Demand
- Step 3 – Scheduling with Concurrent Mode
- Step 4 – Bringing in React Server Components
- Pros & Cons
- ✅ Pros
- ❌ Cons
- Conclusion
The Hydration Problem
Server‑side rendering (SSR) sends a static HTML shell to the browser, then JavaScript “hydrates” it by attaching React’s event listeners and state.
While SSR improves First Contentful Paint (FCP), classic hydration still blocks the main thread:
| Pain Point | Why it happens |
|---|---|
| ⚙️ Long block on JS execution | All component bundles download & execute before the page becomes interactive. |
| 💤 Slow Time‑to‑Interactive (TTI) | The entire app competes for the main thread at once. |
| 🤯 Jank on low‑end devices | Big hydration tasks freeze scrolling & gestures. |
Why Traditional Hydration Breaks Down
- Monolithic – One huge hydration task, regardless of what the user actually sees.
- Priority‑blind – A hidden accordion in the footer hydrates as early as the hero carousel.
- Bandwidth‑heavy – JS for every route ships up front, wasting KBs on first paint.
Learning Patterns frames this as the “Hydration Bottleneck” and urges us to “treat hydration like we treat code‑splitting: progressively.”
Meet Progressive Hydration
Definition – Hydrate only the parts of the UI that matter **when* they matter.*
How it Works
- Islands / Boundaries – Slice your markup into “islands” (interactive) and “static ocean” (just HTML+CSS).
- Lazy Bundles – Each island ships its own JS chunk, deferred or preloaded.
- Event Replay – User events captured before hydration are replayed once the island is ready.
-
Scheduler Integration – React 18’s
startTransition()lets hydration yield to more urgent work.
⛵ Analogy: Instead of refitting the whole ship in dry‑dock, fix one deck at a time while the ship keeps sailing.
Implementation Walk‑through
Step 1 – Identify Hydration Boundaries
/* server.jsx – React 18 SSR */
import { Suspense } from "react";
import Footer from "./islands/Footer.client"; // interactive island
import Hero from "./islands/Hero.client"; // interactive island
import StaticTestimonials from "./components/StaticTestimonials"; // no JS
export default function App() {
return (
<>
<Hero />
<StaticTestimonials />
<Footer />
</>
);
}
- Files ending in
.clientcompile into JS bundles. - Components without that suffix render as pure HTML.
Step 2 – Lazy‑load & Hydrate on Demand
// islands/Hero.client.jsx
"use client";
import { useEffect } from "react";
export default function Hero() {
useEffect(() => {
console.log("Hero island hydrated! 🚀");
}, []);
/* …interactive JSX… */
}
The server streams the markup right away; the JS for Hero downloads only when needed (e.g., via import() or HTTP/2 Push).
Step 3 – Scheduling with Concurrent Mode
import { startTransition } from "react";
function hydrateIsland(importFn) {
startTransition(() => {
importFn().then(({ default: Island }) => {
ReactDOM.createRoot(container).render(<Island />);
});
});
}
-
startTransitiontells React: “lower priority; keep the UI responsive.” - Hydration yields to user input, tackling islands when the thread is free.
Step 4 – Bringing in React Server Components
// app/page.server.jsx (Next.js / React 18)
import ProductList from "./ProductList.server";
export default async function Page() {
const products = await fetch("/api/products").then(r => r.json());
return (
<>
{/* Renders on the server, zero JS sent */}
<ProductList products={products} />
{/* Hydrated client island for the cart */}
<AddToCartButton />
</>
);
}
Server Components stream ready‑to‑use HTML; only truly interactive islands (e.g., AddToCartButton) ship JS and hydrate progressively.
Pros & Cons
✅ Pros
- Faster TTI – Only critical islands block interactivity.
- Lower JS Cost – Each page loads just enough code.
- Graceful Degradation – Static parts work even if JS fails.
- Main‑thread Friendly – Hydration tasks can yield via Concurrent Mode.
❌ Cons
- Complex Build Setup – Need split pipelines for server vs client code.
- Tooling Maturity – RSC & progressive hydration are still evolving.
- Event Replay Edge Cases – Lost events if not buffered correctly.
- Debugging Overhead – Mixed execution contexts (server/client) complicate tracing.
Conclusion
Progressive hydration flips the traditional SSR model on its head: hydrate what matters, when it matters.
With React 18’s Concurrent Mode and the emerging React Server Components architecture, you can ship lighter pages, stay responsive, and delight users—especially on slow networks and devices.
Takeaway: Start small. Convert a footer or sidebar into an island, measure TTI, then iterate. The performance gains compound quickly!
Happy hydrating! 💧
Top comments (0)