What you'll learn
Quick Answer
Hydration is the step where React takes server-rendered HTML and attaches event listeners and state to it, turning static markup into a working app. Between the HTML appearing and hydration finishing, the page looks ready but ignores clicks - that is the flash. If the first client render does not match the server HTML exactly, React logs a hydration mismatch and rebuilds that part of the tree in the browser.
What hydration actually is
When a React app is server-rendered, two things go to the browser:
- The HTML - fully rendered markup, so the user sees content immediately.
- The JavaScript bundle - your component code.
The HTML is inert. Buttons are drawn but nothing happens when you click them, because the event handlers live in the JavaScript, which still has to download, parse, and execute.
Hydration is what happens when that JavaScript runs. React walks the existing DOM, builds its internal component tree on top of the nodes that are already there, and attaches the event listeners and state. The entry point is hydrateRoot rather than createRoot:
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
The key idea: hydrateRoot assumes the DOM is already correct and tries to adopt it. It renders your components, but instead of creating DOM nodes it matches them against the nodes the server sent.
Why the page flashes or feels frozen
The window where the page looks done but does not respond opens in this order:
- HTML arrives. The user sees the page. (fast)
- The browser downloads the JavaScript bundle. (depends on bundle size and network)
- The main thread parses and executes it, then hydration runs. (depends on bundle size and CPU)
- Only now do clicks, inputs, and menus work.
On a fast laptop this is milliseconds. On a mid-range phone with a large bundle it can be several seconds, during which a tap on the menu does nothing. Some setups also show a visible jump at hydration - a dropdown that was open in the HTML snaps shut - because the client's first render disagrees with the server's.
Hydration also competes with everything else on the main thread. In the classic model React hydrates the whole tree in one pass before anything is interactive; React 18's selective hydration lets Suspense boundaries hydrate independently and reprioritise toward a widget the user just clicked. Either way, more client components means more hydration work - which is why "just server-render it" is not a free performance win.
Hydration mismatches: the error you will see
Hydration only works if the first render on the client produces exactly the tree the server produced. When it does not, React logs:
Hydration failed because the server rendered text didn't
match the client. As a result this tree will be regenerated
on the client. This can happen if a SSR-ed Client Component used:
- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which
changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.
- External changing data without sending a snapshot along with the HTML.
- Invalid HTML tag nesting.
"This tree will be regenerated on the client" is the important part: React discards the server HTML for the mismatched subtree and re-renders it from scratch in the browser. You lose the SSR benefit for that section, and the user may see the content visibly swap.
In a test where the server rendered a value of 1000 and the client hydrated with 2000, React logged this error and the DOM ended up showing 2000 - on a mismatch, the client render wins.
What causes a mismatch
Every common cause comes down to one thing: the server and the browser had different information at render time.
- Time and randomness.
new Date(),Date.now(),Math.random(),crypto.randomUUID()- each produces a different value on the server than milliseconds later on the client. - Locale and timezone.
date.toLocaleString()formats with the server's locale during SSR and the user's locale during hydration. "1/2/2026" versus "2/1/2026" is a mismatch. - Browser-only data. Reading
localStorage,window.innerWidth, or a cookie during render. The server has none of these, so it renders one thing and the client another. - Branching on
typeof window. The branch is taken on the client and not the server by design - which is precisely a mismatch. - Invalid HTML nesting. A
<div>inside a<p>, or a<p>inside a<p>. The browser silently repairs the HTML by moving nodes, so the DOM no longer matches what React rendered.
Browser extensions that alter the page before React runs can also trigger it - those you can usually ignore.
Fixing them: two-pass and suppressHydrationWarning
The two-pass pattern. For anything that legitimately only exists in the browser, render the same output as the server on the first client render, then update after mount:
function LocalTime() {
const [time, setTime] = useState(null);
useEffect(() => {
setTime(new Date().toLocaleTimeString());
}, []);
return <span>{time ?? 'Loading...'}</span>;
}
Server and first client render both show "Loading..." - they match, so no error. Then useEffect runs (client only), state updates, and the real time appears on a second render. In testing this produces zero hydration warnings. The cost is a brief flash of the placeholder, which is the honest trade-off.
suppressHydrationWarning. Adding this prop to an element tells React to skip the mismatch check for that one element's text and attributes:
<time suppressHydrationWarning>{new Date().toISOString()}</time>
The gotcha: it only silences the warning. It does not make React re-render the element to match the client. In testing, an element that rendered 1000 on the server and would render 2000 on the client kept showing 1000 after hydration. Use it only for unavoidable one-level differences like a timestamp, never to quiet a real bug.
Reducing the cost of hydration
The mismatch fixes above are patches. The structural fix is to hydrate less.
- Server Components (the Next.js App Router) never hydrate - they carry no client code. Keep display-only parts of the tree as Server Components and make only the interactive leaves Client Components.
- Islands architecture (Astro, Fresh) ships static HTML by default and hydrates only the specific interactive widgets you mark, each one independently. Most of the page never runs JavaScript at all.
- Smaller bundles. Code-split routes and heavy components so hydration has less to execute up front.
- Lazy hydration. Defer hydrating below-the-fold widgets until they scroll into view.
The through-line: hydration cost scales with how much interactive code you ship. Server-render the parts that are just content, and reserve client JavaScript for the parts that genuinely need to respond to the user.
