HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 442 min read

Hydration (SSR vs Interactive)

the myth that SSR makes your page interactive faster

Web developmentweb development / performance

The myth

"We moved to Next/Nuxt/SvelteKit. The HTML arrives already rendered, so users can use the page sooner."

Actually

SSR makes your page VISIBLE sooner. It does nothing for INTERACTIVE — and can push it later than a plain SPA.

The server sends HTML, the browser paints it, buttons appear. But zero event listeners are attached. The framework still has to:

  1. download the JS bundle
  2. parse + execute it
  3. re-render the whole component tree in memory
  4. walk the real DOM, attach listeners, assert markup matches

Steps 3–4 are hydration. Until they finish, your page is a screenshot.

The uncanny valley

FCP at 0.6s, hydration done at 3.4s. For 2.8 seconds the user sees a finished UI and taps things that do nothing. Those clicks are dropped, not queued. A spinner would have been honest; SSR shows a lie.

And hydration is CPU work on the single main thread. A mid-range Android is 4–6x slower than your laptop: 300ms of hydration on your MacBook is 1.5s+ on the device your users actually hold.

Prove it in 30 seconds

DevTools → Performance → CPU throttle 4x → reload. Look for the long "Evaluate Script" block and the long task sitting AFTER first paint. Or instrument it:

// root component
useEffect(() => console.log('hydrated', performance.now()), []);

Compare that number to your FCP. The gap is the lie window. Track INP, not Lighthouse's score — a 98 is mostly a load-speed measurement.

What actually trips people

  • Hydration cost scales with COMPONENT COUNT, not page weight. Compressing images doesn't move it at all.
  • A hydration mismatch (Date.now(), localStorage, locale-formatted numbers during render) makes React discard the server HTML and re-render on the client — you paid for SSR and shipped a slower CSR.
  • Fetching above-the-fold data in useEffect adds a whole round trip AFTER hydration finishes.

Fixes, in order of leverage

  1. Ship less JS: Server Components / islands render markup server-side with zero client bundle.
  2. Hydrate less: next/dynamic with ssr:false, or Astro's client:visible, for below-the-fold widgets.
  3. Stream it: React 18 + <Suspense> hydrates selectively, and a click on a not-yet-hydrated boundary jumps that boundary to the front of the queue.
  4. Different model entirely: Qwik "resumes" from serialized state instead of replaying the render — no hydration pass at all. Worth knowing the idea even if you never adopt it.

The one-line mental model

SSR buys you pixels. Hydration buys you behavior. They are separate purchases, and you pay for the second one in main-thread milliseconds on someone else's phone.

Your exercise

Take your heaviest page. CPU throttle 4x, record from FCP to the first click that actually works — write down the ms. Then list your 3 biggest hydrated components and ask each: "does this need to be interactive on load?" Move one behind a lazy/no-SSR boundary and re-measure. What was the delta?