DEV Community

Cover image for Progressive Hydration in React: Taming the Hydration Monster
Md Enayetur Rahman
Md Enayetur Rahman

Posted on

Progressive Hydration in React: Taming the Hydration Monster

Learning Patterns Deep‑Dive – Pattern 45

Based on “Learning Patterns” by Lydia Hallie & Addy Osmani


Table of Contents


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

  1. Monolithic – One huge hydration task, regardless of what the user actually sees.
  2. Priority‑blind – A hidden accordion in the footer hydrates as early as the hero carousel.
  3. 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

DefinitionHydrate only the parts of the UI that matter **when* they matter.*

How it Works

  1. Islands / Boundaries – Slice your markup into “islands” (interactive) and “static ocean” (just HTML+CSS).
  2. Lazy Bundles – Each island ships its own JS chunk, deferred or preloaded.
  3. Event Replay – User events captured before hydration are replayed once the island is ready.
  4. 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 />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • Files ending in .client compile 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… */
}
Enter fullscreen mode Exit fullscreen mode

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 />);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode
  • startTransition tells 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 />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Server Components stream ready‑to‑use HTML; only truly interactive islands (e.g., AddToCartButton) ship JS and hydrate progressively.


Pros & Cons

✅ Pros

  1. Faster TTI – Only critical islands block interactivity.
  2. Lower JS Cost – Each page loads just enough code.
  3. Graceful Degradation – Static parts work even if JS fails.
  4. Main‑thread Friendly – Hydration tasks can yield via Concurrent Mode.

❌ Cons

  1. Complex Build Setup – Need split pipelines for server vs client code.
  2. Tooling Maturity – RSC & progressive hydration are still evolving.
  3. Event Replay Edge Cases – Lost events if not buffered correctly.
  4. 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)