Rendering

Hydration (ハイドレーション)

Hydration is the process where client-side JavaScript retrofits interactive behavior onto statically-generated HTML from the server or build. Every SSR / SSG setup goes through this step — React's hydrateRoot, Vue's createSSRApp, Astro's Islands, and so on.

Metrics Primer: FCP / LCP / TTI

The swim-lane diagram below marks FCP / LCP / TTI milestones. Knowing what each metric actually measures upfront makes the diagram much easier to read.

FCP First Contentful Paint — first visual content painted
The moment text, images, SVG, or non-empty canvas is first painted on the browser viewport. A background-color-only screen doesn't count. A Core Web Vitals secondary metric — under 1.8 s is considered good.
LCP Largest Contentful Paint — largest content painted
When the largest content element (hero image, big heading, etc.) is painted. A proxy for 'meaningful first paint' and a primary Core Web Vitals metric. Under 2.5 s is good; over 4 s needs work.
TTI Time to Interactive — when interactivity starts
The moment the page is fully interactive — all event listeners attached and the main thread free of Long Tasks for 5 seconds. Dropped from the Core Web Vitals primary set (replaced by INP) in 2024, but still important as the 'feels usable' moment for real users.

What Hydration Is

Hydration is the process where client-side JavaScript retrofits interactivity onto statically-generated HTML from the server or build. It's a necessary step for any SSR / SSG setup, implemented differently in React / Vue / Svelte / Astro but conceptually the same. web.dev defines it most concisely:

"Running client-side scripts to add application state and interactivity to server-rendered HTML."

web.dev: Rendering on the Web (CC BY 4.0)

React's docs describe what hydration actually delivers as a user experience:

"Hydration turns the initial HTML snapshot from the server into a fully interactive app that runs in the browser."

React: hydrateRoot (CC BY 4.0)

Rendering Lifecycle

Nine phases from server-rendering completion (01) to hydration completion (08). After reaching FCP / LCP at phase 03, the 'visible but unresponsive' uncanny valley persists while JS executes — ending at TTI in phase 08. This gap is hydration's fundamental cost.

Server
Network
Browser
JS Engine
Display
  1. 01

    renderToString emits HTML + embedded state

    01 Server renderToString emits HTML + embedded state

    On the server, React / Vue / Svelte calls renderToString(). Alongside the complete HTML, state needed for client reconstruction is embedded in a <script> tag. Per React docs: 'Server rendering creates an illusion that the app loads faster by showing the HTML snapshot of its output.'

  2. 02

    Transfer HTML + state

    02 Network Transfer HTML + state

    The HTTP response returns the complete HTML. The JS bundle download starts in parallel or immediately after.

  3. 03

    HTML parse → Layout → Paint

    03 Browser HTML parse → Layout → Paint

    The browser parses the complete HTML and runs DOM → CSSOM → Layout → Paint. The user can now see content. But clicks still don't respond — the pre-hydration state discussed below.

  4. FCP / LCP reached — content visible but not interactive
  5. 04

    JS bundle parse + compile

    04 JS Engine JS bundle parse + compile

    Once the JS bundle arrives, the engine (V8, etc.) parses and compiles it. Main-thread blocking begins in earnest. Per web.dev: 'The primary downside of server-side rendering with rehydration is that it can have a significant negative impact on TBT and INP.'

  6. 05

    hydrateRoot() invoked / rebuild VDOM

    05 JS Engine hydrateRoot() invoked / rebuild VDOM

    Per React docs: 'hydrateRoot lets you display React components inside a browser DOM node whose HTML content was previously generated by react-dom/server.' The component tree executes and builds a virtual DOM from scratch. The real DOM is not yet touched.

  7. 06

    Match against existing DOM (reconciliation)

    06 JS Engine Match against existing DOM (reconciliation)

    Walk the virtual DOM against the already-painted real DOM node-by-node. Per React docs: 'React will attach to the HTML that exists inside the domNode, and take over managing the DOM inside it.' If markup matches, the DOM isn't re-created — React just 'adopts' it.

  8. 07

    Attach event listeners + initialize state

    07 JS Engine Attach event listeners + initialize state

    Event listeners defined in components (onClick, etc.) are attached to matched DOM nodes. State management (useState / Vuex / pinia) is initialized. This phase is exactly what web.dev defines hydration as: 'Running client-side scripts to add application state and interactivity to server-rendered HTML.'

  9. 08

    useEffect / onMounted fires

    08 JS Engine useEffect / onMounted fires

    Lifecycle hooks fire, kicking off side effects like API fetches, DOM references, and external library initialization. Browser APIs that couldn't be used during SSR (window / localStorage / IntersectionObserver) become accessible here for the first time.

  10. TTI reached — the page is truly usable from here
  11. 09

    (On mismatch) subtree re-render

    09 JS Engine (On mismatch) subtree re-render

    If server HTML and client VDOM disagree, React warns and re-renders that subtree from scratch. Per React docs: 'React recovers from some hydration errors, but you must fix them like other bugs. In the best case, they'll lead to a slowdown; in the worst case, event handlers can get attached to the wrong elements.'

Hydration Strategies

To mitigate standard hydration's weakness (TBT / INP regression), several variants have been proposed and implemented. Design philosophy and implementations of each:

Standard Hydration

React hydrateRoot / Vue createSSRApp

The baseline approach: hydrate the entire page HTML at once. Using hydrateRoot() (React) or createSSRApp(...).mount() (Vue), the same component tree as the server is rebuilt client-side and attaches listeners across the whole tree. Because the full bundle must download and execute first, this path has the longest TTI.

Progressive Hydration

web.dev が提唱する方式

Per web.dev: 'Individual pieces of a server-rendered application are 'booted up' over time, instead of the current common approach of initializing the entire application at once.' Instead of bulk hydrating, parts are hydrated sequentially — deferring off-screen or low-priority UI to reduce initial load.

Partial / Selective Hydration (Islands)

Astro Islands Architecture

Per Astro docs: 'render HTML pages on the server, and inject placeholders or slots around highly dynamic regions ... that can then be 'hydrated' on the client into small self-contained widgets.' Most of the page ships as pure static HTML; only the interactive 'islands' carry a JS runtime. Astro requires explicit client:load / client:visible / client:idle directives to control when each island boots.

Selective Hydration (Suspense-based)

React 18+ (公式: Suspense)

Per React docs (Suspense reference): 'React includes under-the-hood optimizations like Streaming Server Rendering and Selective Hydration that are integrated with Suspense.' Streaming SSR and selective hydration operate via boundaries — hydration happens incrementally per boundary. Unlike Astro Islands (developer-declared), the partitioning emerges naturally from Suspense boundary design.

Resumability (no hydration)

Qwik (公式: Resumable)

Per Qwik docs: 'Resumability is about pausing execution on the server and resuming execution on the client without having to replay and download all of the application logic.' On the contrast with hydration: 'All other frameworks' hydration replays all the application logic on the client. Qwik instead pauses execution on the server, and resumes execution on the client.' On serialization: 'Qwik collects component boundary information as part of the SSR/SSG and then serializes that information into HTML.' In short, the client never replays the whole app — it resumes the specific piece needed at the moment of interaction.

Frequently Asked Questions

Q. Why is hydration needed? Isn't server-rendered HTML enough?
Because HTML alone can't respond to click / input. The server emits a 'still image of the UI' with no event handlers or state management. Per React docs: 'The user will spend some time looking at the server-generated HTML before your JavaScript code loads' — a visible-but-unusable window is inevitable. Hydration is what runs JavaScript client-side to retrofit behavior onto that still image. If a fully static HTML is enough (SSG + pure static), no hydration is needed; it becomes necessary the moment dynamic UI is involved.
Q. What's the problem with a 'hydration mismatch'?
It's the state where server-rendered HTML and client-re-rendered output disagree. React docs on severity: 'In the best case, they'll lead to a slowdown; in the worst case, event handlers can get attached to the wrong elements.' Worst case: a button ends up with another element's handler. The basic fix is 'make server and client produce identical output'. Common causes: (a) using Date.now() / Math.random() in render, (b) branching on typeof window, (c) reading localStorage during the initial render. Moving these into useEffect usually resolves it.
Q. Why does hydration hurt TBT / INP?
web.dev puts it plainly: 'The primary downside of server-side rendering with rehydration is that it can have a significant negative impact on TBT and INP.' Reasons: (a) parsing+executing the JS bundle blocks the main thread for a long time, (b) rebuilding the component tree from scratch is computationally expensive, (c) input doesn't work until hydration completes, (d) as web.dev notes, 'server-rendered pages can appear to be loaded and interactive, but can't actually respond to input until the client-side scripts for components are executed.' The longer this 'visible but unresponsive' uncanny valley lasts, the worse the metrics. The mitigation is progressive / partial hydration (see below).
Q. How is Astro Islands different from standard hydration?
Astro's stance is 'zero JS by default'. Per Astro docs: 'By default, Astro will automatically render every UI component to just HTML & CSS, stripping out all client-side JavaScript automatically.' Developers then explicitly opt interactive parts in via client:load / client:visible etc., treating them as 'islands' to hydrate. Standard hydration (React hydrateRoot) assumes 'the entire page runs on JavaScript'; Islands takes the opposite stance — 'mostly static, interactive only where specified'. The practical result is a dramatic drop in shipped JS.
Q. What's the relationship between React Server Components and hydration?
Per React docs (Server Components): 'Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server.' The defining property: 'Server Components are not sent to the browser, so they cannot use interactive APIs like useState.' Also: 'The bundle does not include the expensive libraries needed to render the static content.' Server Components never ship as client JS — only their output (HTML / serialized result) does. Hydration narrows to Client Components only, shrinking both bundle size and VDOM reconstruction. Conceptually close to Astro Islands, but offered as an integrated React API.

Design Signals

Hydration fits here

SSR apps where the whole page is interactive

For pages where almost everything is interactive (post-login dashboards, etc.), the default hydration strategies of Next.js / Nuxt / SvelteKit (renderToString → hydrateRoot, and similar) are adequate. React docs introduce hydrateRoot for turning 'the initial HTML snapshot from the server into a fully interactive app' — exactly this use case. Further optimization should wait on measured INP / TBT regressions.

Hydration fits here

Content-centric pages with localized interactivity

For pages that are 99% read-only with 1% interactive islands (blog / docs / product pages with cart buttons or search boxes), partial hydration via Astro Islands or React RSC is a game-changer. Astro's default of 'stripping out all client-side JavaScript automatically' shines here.

Hydration fits here

Struggling with Core Web Vitals TBT / INP

If measurements show TBT / INP regression traced to 'hydration takes too long', consider progressive hydration / selective hydration (React 18+ Suspense boundaries) / Islands. As web.dev notes, this is a well-known side effect of 'SSR with rehydration'.

No hydration needed

Pure CSR (SPA) setup

In a CSR-only SPA, there's no server-rendered HTML to begin with — so hydration doesn't happen. The browser receives empty HTML and JavaScript renders everything. Hydration becomes a concern only when SSR / SSG enters the mix.

No hydration needed

Fully static sites with no JS

For plain SSG configurations that ship pure static HTML with no JS (docs sites / blogs / some LPs), hydration is unnecessary. As with Astro's default behavior, if completed HTML is enough, that path is fastest and lightest.

Common Pitfalls

Ignoring hydration mismatches

It's tempting to ignore the dev-build warnings because 'it still works', but React docs warn explicitly: 'React recovers from some hydration errors, but you must fix them like other bugs.' Worst case, handlers attach to wrong elements, lurking as non-visual bugs. Root causes are typically Date.now() / Math.random() / UA branching / typeof window checks; the standard fix is moving that code into useEffect.

Click races before hydration completes

In SSR apps with fast FCP / LCP but slow TTI, users click 'visible buttons' before hydration finishes, and those clicks get dropped. Per web.dev: 'Server-side rendered pages can appear to be loaded and interactive, but can't actually respond to input until the client-side scripts for components are executed.' Fix: As React docs (Suspense reference) note, 'React includes under-the-hood optimizations like Streaming Server Rendering and Selective Hydration that are integrated with Suspense' — partition with boundaries and rely on selective hydration. Alternatively, design a visually-distinct 'not-yet-hydrated' state.

State double-serialization bloating the payload

Per web.dev: 'Rehydration can cause more problems than just delayed interactivity' — both the server-rendered HTML and the serialized data for client-side state reconstruction (__NEXT_DATA__, etc.) are embedded in the HTML, effectively doubling the payload. Especially severe when the entire API response gets serialized. Fixes: narrow the state to what's truly needed on the client via Server Components / Islands, or stream it post-response via streaming SSR.

Crashing by using browser APIs during SSR render

On Node.js SSR, window / document / localStorage / IntersectionObserver don't exist. Touching them during the render function throws ReferenceError on the server. Three fixes: (a) guard with typeof window !== 'undefined' (but this itself can cause hydration mismatches — use carefully), (b) move the code into useEffect / onMounted (client-only execution), (c) per Next.js docs (Lazy Loading guide): 'If you want to disable prerendering for a Client Component, you can use the ssr option set to false' — opt the component out of SSR via dynamic(() => import(...), { ssr: false }).