Back to React
React
medium
mid

How do you handle SSR and hydration in complex React applications?

Hydration is the client running React to attach handlers to server HTML. In complex apps: prevent mismatches (same data, same time, same flags on server + client); code-split + lazy-hydrate heavy below-the-fold parts; use Suspense + streaming so hydration is incremental; use RSC to skip hydration for non-interactive UI; defer non-critical to `requestIdleCallback`. Mismatches usually mean impure render.

6 min read·~25 min to think through

Hydration in a real app is rarely "just hydrateRoot and you're done." Complex apps hit mismatches, slow hydration, and INP regressions. Handling it well means preventing mismatches and breaking up hydration work.

1. What hydration is

Server sends HTML. Client downloads the JS bundle. React runs the same component tree on the client and attaches event handlers and refs to the existing HTML. If the rendered output matches, React reuses the HTML; if not, you get a mismatch warning (and React 18 may discard the server HTML for that subtree).

2. Common mismatch causes

Time / dates rendered differently

jsx
<p>{new Date().toLocaleString()}</p>   // server time ≠ client time

Fix: format on the server with a stable locale/timezone, or render a placeholder server-side and update on mount.

Random ids

jsx
const id = useMemo(() => Math.random(), []);

Fix: useId() (React 18) — deterministic across server and client.

Browser-only globals

jsx
const width = window.innerWidth;   // crashes server, mismatches client

Fix: guard with typeof window !== "undefined" or move into useEffect.

Feature flags

If server and client read feature flags from different sources (env vs cookie), they diverge. Fix: pass flags from server to client deterministically (props, serialized state).

Conditional rendering on user agent / locale

Same root cause as above — server can't know all client conditions.

Time-of-day / random ad slot

Don't render server-side; defer to useEffect.

3. Suppressing intentional mismatches

For rare cases where the difference is intentional (e.g., showing local time only client-side), wrap the element:

jsx
<span suppressHydrationWarning>{clientOnlyTime}</span>

Use sparingly — it suppresses warnings but mismatches still cost.

4. Breaking up hydration

Hydrating a large app is one long task by default — bad for INP. Mitigations:

Code split per route

React.lazy for route-level components. The initial bundle is smaller; hydration is smaller.

Lazy hydration / island architecture

Astro popularized this: only interactive components hydrate; static content ships zero JS.

In React: use React Server Components (in Next.js App Router) — server components don't hydrate at all.

Selective hydration via Suspense

React 18's hydrateRoot + Suspense lets React hydrate the most important parts first; below-the-fold parts can wait. Combine with streaming SSR.

Defer non-critical components

jsx
const HeavyChart = lazy(() => import("./HeavyChart"));

// Or with explicit visibility:
const [show, setShow] = useState(false);
useEffect(() => {
  const obs = new IntersectionObserver(([e]) => e.isIntersecting && setShow(true));
  obs.observe(ref.current);
  return () => obs.disconnect();
}, []);

5. Streaming SSR + Suspense

Server flushes the shell HTML immediately, then sends slower data widgets as they resolve. React on the client hydrates the shell first, then progressively hydrates the streamed parts.

jsx
<Suspense fallback={<Skeleton />}>
  <SlowDataWidget />
</Suspense>

Avoid blocking the entire SSR on the slowest data fetch.

6. Pass data, don't re-fetch

If the server fetched user data to render, embed it in the HTML (<script>window.__INITIAL_STATE__ = {...}</script>) and read on the client — don't re-fetch the same data.

React Query has Hydration utilities for this; Next.js handles it automatically.

7. Watch INP after hydration

Hydration is a long task → bad TBT/INP. Tools:

  • Long-task observer to detect hydration spikes.
  • web-vitals for INP in production.
  • Profile a hydration with React Profiler.

8. RSC eliminates hydration for parts

React Server Components render on the server and ship serialized output — no client JS, no hydration. Only the components you mark as client components hydrate. For large apps with lots of static content, this is the biggest hydration optimization available.

9. Patterns to avoid

  • Big monolithic hydration root — split.
  • Re-fetching data on the client that the server already had — hydrate state.
  • Random / time-dependent values in render — drift causes mismatches.
  • Browser globals at the top level — guard or defer.

Interview framing

"Hydration is the client running React to attach handlers to server HTML. In complex apps the two problems are mismatches and cost. Mismatches come from impure render: time, dates, random ids, browser globals, feature flags that differ between server and client. Use useId for stable ids, format with a stable locale on the server, guard browser globals, and serialize feature flags. Cost — hydration as one long task — kills INP. Mitigate with route-level code splitting, lazy hydration of below-the-fold widgets, Suspense + streaming SSR so slow widgets don't block the shell, and especially React Server Components which skip hydration entirely for non-interactive parts. And hydrate state from the server payload — don't re-fetch what was already rendered."

Follow-up questions

  • What causes a hydration mismatch?
  • Why is hydration expensive in big apps?
  • How does RSC change the hydration cost equation?
  • What's selective hydration in React 18?

Common mistakes

  • Random / time / Math.random in render.
  • window/document at top level.
  • Suppressing hydration warnings instead of fixing.
  • Re-fetching what the server already rendered.
  • Big monolithic hydration roots.

Performance considerations

  • Hydration cost = TBT / INP problem. Smaller bundles + lazy hydration + RSC are the levers.

Edge cases

  • Feature flag rollout across server/client.
  • Different user agent server vs client.
  • Hydrating with a different prop subtree (route mismatch).

Real-world examples

  • Next.js App Router with RSC.
  • Remix with route-level data + streaming.
  • Astro islands.

Senior engineer discussion

Seniors prevent mismatches by writing pure render, code-split aggressively, reach for streaming + Suspense + RSC for cost, and measure hydration in production via long-task observers + INP RUM.

Related questions