In plain words
Server-side rendering hands the browser finished HTML, so the page paints before any JavaScript arrives. React's docs call that output an "initial HTML snapshot", and on its own it is inert. Vue's SSR guide shows it with a counter whose button does nothing until the framework loads: "The HTML is completely static on the client since we are not loading Vue in the browser."
Hydration is the step that makes that markup respond. The framework runs your components again in the browser, walks the DOM it already has, and wires each existing node to the component that owns it. Hydration keeps the snapshot and plugs the event handlers and state in behind it, so no DOM nodes are thrown away and re-created.
The same word means something unrelated in data access. In Doctrine, a PHP object-relational mapping (ORM) library, the "hydration mode" decides how SQL result rows are turned into objects or arrays. This entry covers the browser meaning only.
How it works
- Rendering comes first: the server, or a build step, turns the components into an HTML string, for example with
react-dom/server. - Next, the browser parses and paints that HTML. Buttons and forms are visible but do not respond yet.
- Once the JavaScript bundle loads, it calls the framework's hydration entry point instead of its normal mount:
hydrateRootin React,createSSRApp()in Vue,provideClientHydration()in Angular. - During hydration the framework renders the component tree once more, matches the result against the DOM nodes already on the page, and attaches event listeners and state to them.
- From here on the page behaves like any client-rendered app.
In React, the client side of that sequence is a single call. This is the example from the React reference:
import './styles.css';
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(
document.getElementById('root'),
<App />
);Older tutorials show ReactDOM.hydrate instead. React 16 added it in September 2017 to give hydration an explicit API, and React 18 replaced it with hydrateRoot in March 2022, noting that "New features in React 18 don't work without it."
We ran this on React 19.3.1 (Bun 1.4.0, jsdom 31.0.0): render a counter button with renderToString, put the HTML in a DOM, hydrate it, then check whether the button element is the same object before and after. With matching labels it is, and the click runs on the node the server produced (the line labels are printed by our script):
server HTML: <button>Clicks<!-- -->: <!-- -->0</button>
same <button> node reused: true
after click: <button>Clicks<!-- -->: <!-- -->1</button>
recoverable errors: noneWhen the HTML does not match
Hydration only works if the client's first render produces the same markup the server sent. React's reference says hydrateRoot() "expects the rendered content to be identical with the server-rendered content. You should treat mismatches as bugs and fix them." We changed the client label from "Server" to "Client" and ran it again. React threw away the server's button, rendered a new one, and reported this through onRecoverableError:
same <button> node reused: false
after click: <button>Client: 1</button>
Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client.The React 19 release post quotes this error as "server rendered HTML didn't match"; for a text mismatch, 19.3.1 says "server rendered text". React 18 worded it differently again, as "Hydration failed because the initial UI does not match what was rendered on the server", the version behind a Stack Overflow question with 690,699 views. Search for all three when you grep logs. The full message lists the usual causes, and the Next.js error page adds a few more:
| Cause | Why the two renders differ |
|---|---|
if (typeof window !== 'undefined') in render | The server takes one branch, the browser takes the other. |
Date.now(), Math.random(), locale date formatting | The value changes between the server run and the client run, or the time zones differ. |
Invalid nesting such as <div> inside <p> | The browser's HTML parser "corrects" the markup, so the DOM no longer has the shape the server rendered. |
| Browser extensions, or a CDN that rewrites HTML | Something edits the page before the framework hydrates it. Next.js names Cloudflare Auto Minify as one example. |
Recovery is not a fix. React says that in the best case a mismatch costs a slowdown and "in the worst case, event handlers can get attached to the wrong elements", and it does not guarantee that differing attributes get patched. Vue also recovers by discarding the wrong nodes and mounting new ones, at a rendering cost.
The documented fix is to render the same thing on both sides, then switch to client-only content after hydration, which in React means setting state in useEffect and accepting a second render. suppressHydrationWarning in React and data-allow-mismatch in Vue 3.5+ silence differences that cannot be avoided; Next.js gives a timestamp as the example. React's flag only works one level deep and does not patch the mismatched text.
Vue and React recover from a mismatch; Angular fails hydration and throws:
| Framework | Hydration entry point | On a mismatch | Opt-out |
|---|---|---|---|
| React | hydrateRoot | Regenerates the tree on the client and reports a recoverable error; attribute differences may not be patched. | suppressHydrationWarning (one level deep) |
| Vue | createSSRApp() then mount | Tries to recover, discarding wrong nodes and mounting new ones. | data-allow-mismatch (Vue 3.5+) |
| Angular | provideClientHydration() | Hydration fails and throws a DOM mismatch error. | ngSkipHydration skips hydrating a component and its children |
Why it matters
Hydration is why a server-rendered page can look finished and still ignore your click. web.dev's Rendering on the Web files this under "One app for the price of two": the UI "doesn't become interactive until after bundle.js has finished loading and executing", and the data behind the HTML is usually sent a second time as serialized script tags. On mobile, the article warns, "this can take minutes, confusing and frustrating the user." When clicks still feel slow after the page is interactive, the guide to optimize Interaction to Next Paint covers finding the script that blocks them.
Much recent framework work has gone into doing less of it. Before React 18, "You have to hydrate everything before you can interact with anything", as the React 18 working group's post on the new SSR architecture put it. React 18 added selective hydration, which hydrates <Suspense> boundaries separately and does the one the user is interacting with first. Angular ships incremental hydration. Islands architecture hydrates only the interactive regions of an otherwise static page, and Qwik's resumability skips the replay altogether, a split the Qwik vs Astro comparison walks through.
Common confusions
| Term | What it is | How it differs from hydration |
|---|---|---|
| Server-side rendering (SSR) | Rendering the app on the server to send HTML, rather than JavaScript, to the client. | SSR produces the HTML. Hydration is the later client step that makes it respond. |
| Rehydration | Usually a synonym. | web.dev notes it is used interchangeably but "implies regularly updating the DOM with the latest state". Most framework docs just say hydration. |
| Progressive, partial or selective hydration | Hydrating pieces of the page over time, or only some pieces. | Still hydration, split up. React 18's selective hydration and Angular's incremental hydration are framework versions of it. |
| Resumability | Qwik's model: "pausing execution on the server and resuming execution on the client". | Hydration re-executes component code in the browser. Qwik's docs say other frameworks' hydration "replays all the application logic on the client"; resumability avoids that replay. |
| Islands architecture | A page of static HTML with small interactive regions. | A page layout that uses hydration per island instead of for the whole document. |