Hydration
The client attaches behaviour and state to markup that already exists. Until it finishes, the page looks finished and is not.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
How does server-rendered HTML become an interactive application, and what is the page during the interval in between?
Someone can already see the button. They want to press it.
The server sent the markup, the client runs the same components over it, and behaviour attaches. It is the same render twice, so it should be quick and invisible.
It is not invisible: it is the longest period in most page loads during which the interface looks complete and answers nothing. This is the uncanny valley of web performance, and users experience it as the page ignoring them (Interaction Responsiveness).
- It is not invisible: it is the longest period in most page loads during which the interface looks complete and answers nothing. This is the uncanny valley of web performance, and users experience it as the page ignoring them (Interaction Responsiveness).
- It is not free either. The client rebuilds the entire component tree in memory to work out which node owns which listener and which state, and that walk is main-thread work proportional to the size of the page, not to the interactive part of it (The Real Cost of JavaScript).
- It cannot start until the bundle has downloaded, parsed and executed, so every byte you ship delays interactivity even when the pixels have been on screen for a while (Bundle Analysis).
- A click during the interval usually does nothing at all — no listener exists yet. It does not queue, it is not retried, and nothing tells the user their press was discarded (How an Event Is Dispatched).
- It runs as one long task by default in many frameworks, so it also blocks the browser from responding to anything else, including scroll handling and paint (Long Tasks).
- If the client's render disagrees with the markup, the framework may discard the server output and rebuild that subtree, throwing away the early paint it was there to preserve (Hydration Mismatch).
What is actually happening
In the browser, not in the framework.
- The document already contains the DOM. Hydration does not create nodes; it walks the existing tree in step with a fresh client render and, for each component, records where its state lives and attaches its event listeners (The DOM Is Not Your HTML).
- The serialized state embedded by the server is parsed first, so the client render starts from the same inputs the server had. Without it, the client would fetch again and the first render would disagree (Server-Side Rendering).
- Event listeners are the point. The markup is inert not because it lacks styling but because no handler is bound to it and no framework state exists behind it (How an Event Is Dispatched).
- The walk is depth-first over the whole tree, including every region that will never be interactive, because the framework has no way to know which regions those are unless the component model tells it (Islands and Partial Hydration).
- Some frameworks compile away most of this: with a compiler that knows exactly which nodes are dynamic, hydration attaches a small number of listeners rather than reconstructing a tree, and the cost profile is materially different (The Svelte Mental Model, The Solid Mental Model).
- Nothing about hydration is a browser feature. The browser sees ordinary script that reads the DOM and calls
addEventListener; the cost and the gap are properties of the framework's design, not of the platform (The Browser Is a Runtime).
What this makes the browser do
And which of it is avoidable.
- Parse and execute the whole bundle, on the main thread, before any of the work below can begin (The Real Cost of JavaScript).
- Parse the serialized state, which on a data-heavy page is a real JSON parse of a real payload (What Serialization Costs lives in Backend Engineering; the browser-side cost is main-thread time).
- Build the client component tree and traverse the DOM alongside it, comparing structure as it goes.
- Attach listeners, run effects, and commit any corrections — each of which can invalidate style and layout on a tree that is already painted (The Cost of a Change).
- Avoidable: all of it, for regions that are not interactive. That is not a micro-optimisation; it is usually most of the page (Islands and Partial Hydration).
- Avoidable in part: doing it as one task. Yielding between chunks lets the browser answer input mid-hydration, at the cost of finishing later (Yielding and Scheduling).
The uncanny valley, drawn
Every other timeline in this module is read left to right. This one is read as a distance: how far apart are Content visible and Interactive, and what is happening in between. That distance is the only thing hydration adds to a page, and it is the reason "we server-render, so we are fast" is a claim that needs a second sentence.
What makes it a genuine trap rather than a minor cost is that the page is at its most convincing precisely when it is least functional. A blank page communicates "wait" perfectly. A complete-looking page with a Save button communicates "press me", and it will keep communicating that for the whole interval.
- HTML arrives — Complete markup, whether it came from a per-request render or from a build.
- Content visible — Everything is on screen. To a user, and to almost every screenshot-based test, the page is done.
- JS downloaded — The gap begins here and its first term is bytes. This is why bundle size is an interactivity problem rather than only a bandwidth one.
- Data ready — Already embedded in the document as serialized state, so the client does not refetch — it parses.
- Hydration — Parse, execute, rebuild the tree, walk the DOM, attach listeners. One long main-thread task in the default case, during which nothing else runs.
- Interactive — The first moment a press does anything. Everything between this row and Content visible was a page pretending.
The rows to compare are the second and the last. Shipping less code shortens the third row; hydrating less shortens the fifth; neither moves the second, which is the point.
What the pass actually does
Naming the steps makes it clear why "just attach the listeners" understates it. In a framework that reconstructs its component tree to know where listeners belong, the reconstruction is the expensive part, and it happens over the whole document rather than over the interactive parts of it.
It also makes clear where the mismatch problem enters. Step four is a comparison, and a comparison can disagree — at which point the framework has to choose between trusting markup it believes is wrong and discarding paint it worked to produce early (Hydration Mismatch).
- 1Download and execute the bundle
Brings the component definitions and the framework runtime into the page.
fails by A chunk that 404s after a deploy, leaving a page that is permanently inert and looks finished (Content-Hashed Assets).
- 2Parse the serialized state
Reconstructs the inputs the server rendered from, so the client can render the same thing.
fails by A payload large enough that the parse is itself a long task, on a page whose data was never needed on the client.
- 3Build the client tree
Runs the components to produce the structure the framework expects to find in the DOM.
fails by Executing side effects at module scope or during render that the server did not run, so the two environments diverge before comparison starts.
- 4Reconcile against the DOM
Walks the existing nodes in step with that structure, matching them up.
fails by A structural disagreement, which forces the framework to discard the server markup for that subtree and rebuild it (Hydration Mismatch).
- 5Attach listeners and state
Binds handlers to nodes and wires each component to its state container.
fails by Nothing visible — this is the step users are waiting for, and its completion is not announced anywhere.
- 6Run effects
Fires subscriptions, measurements and anything that needed a real DOM.
fails by A measurement-driven layout change immediately after paint, moving content the user is already reading (Layout Thrashing).
Only the fifth step is what people mean by "hydration". The first four are what it costs.
A control that exists before its behaviour does
The accessibility framing is the clearest one, because it removes the ambiguity that visual design introduces. A sighted user might interpret a slightly grey button as "not ready yet". A screen reader reads the accessibility tree, and the accessibility tree says this is an enabled button. There is no visual nuance available; there is only what the semantics claim.
So the requirement is straightforward: during the gap, the markup must not claim more than it can do. Either the control genuinely works without JavaScript — a link, a form, a native disclosure — or its markup says it is not ready, and hydration is what makes it ready.
semantics A native button, a or input — never a div with a role, which has no behaviour to fall back on. If the control cannot act yet, it carries disabled (or aria-disabled where it must stay focusable) in the server output, and the attribute is removed on hydration.
| Tab | Reaches the control, because tab order comes from the DOM and the DOM is already complete. This works during the gap whether you planned for it or not. |
| Enter | On a real a or a submit button in a real form, performs the native action with no JavaScript at all — the strongest reason to prefer native elements in server-rendered pages (What Native Elements Already Do). |
| Space | Activates a native button. On a div with a click handler it scrolls the page instead, and during the gap there is no handler to intercept it either way. |
| Escape | Nothing, until a handler exists. Any dismissible surface rendered open by the server is untrappable and unclosable during the gap, which is why they should not be rendered open. |
- — Focus order is live from first paint. Assume users reach every control before any of them works.
- — Do not move focus when hydration completes — the user has been reading and navigating the whole time.
- — If a control is disabled in server markup and enabled on hydration, do not also move focus to it; enabling it is enough.
- — A disabled control announces its state, which is honest. A control that is enabled and inert announces nothing, which is not.
- — If the page defers a meaningful capability until hydration, a polite status message saying so is better than silence.
- — Any value the client corrects after hydration should be announced if a user could have acted on the previous one (Live Regions and Announcement).
usually broken by The pattern is broken by rendering fully enabled interactive markup on the server and relying on the gap being short. It is short on the machine it was built on. On a mid-range phone over a slow connection, it is long enough for a person to press the button, wait, press it again, and give up — and nothing in the interface ever acknowledged any of it.
How to build it
Most important first.
- Treat the gap as a designed state, not an accident. Decide what the page does when someone interacts during it, and make that decision visible in the code rather than emergent (Loading, Error, Empty — The States You Did Not Render).
- Ship less. The gap is bounded below by download plus parse plus execute plus walk, and the only lever that moves all four is shipping less code for this route (Code Splitting).
- Hydrate less. If the page is mostly content, hydrating the content is pure waste — the whole argument of Islands and Partial Hydration.
- Prefer native elements that work before your code does. A real
anavigates, a realformsubmits, a realdetailsopens — all without hydration, because the browser implements them (What Native Elements Already Do). - Where a control genuinely cannot work before hydration, disable it in the server output and enable it on hydration, so its state is honest rather than optimistic (Native Forms First).
- Consider recording input during the gap and replaying it after — some frameworks do this for you, and it converts a discarded click into a delayed one (How an Event Is Dispatched).
- Prioritise: hydrate what is in the viewport, or what the user is most likely to touch, before the footer (content-visibility).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- This is the accessibility problem of server-rendered applications, and it is structural rather than incidental: the page presents controls that announce themselves as operable and are not. A screen-reader user hears "Save, button", presses Enter, and nothing occurs — with no error, no announcement and no explanation (Accessible Component Patterns).
- Focus is real during the gap. Tab order comes from the DOM, and the DOM is complete, so a keyboard user can reach every control in the page before a single one of them works (Keyboard Operability).
- If a control cannot act yet, say so in the markup the server sends. A
disabledattribute is announced; a button that silently does nothing is not, and the second is much worse than the first (Semantics Before ARIA). - Never move focus when hydration completes. The user has been navigating a complete-looking document for the entire interval, and yanking focus to the top because the application "started" destroys whatever they were doing (Focus Management).
- A correction after hydration — a value replaced, a region re-rendered — changes content a screen reader may already have announced. If the change is meaningful, announce it; if it is not, it should not have been rendered differently in the first place (Live Regions and Announcement).
- The interval is longer on slow devices, which correlates with the users least likely to have a fast connection and most likely to be relying on assistive technology on the same hardware (The Real Cost of JavaScript).
What can go wrong
- Silent input loss. The user presses a button, nothing happens, they press it again, and when hydration completes both presses land — or neither does (Interaction Responsiveness).
- A form that submits with client-side validation missing, because the markup was there and the handler was not — and if the server does not validate too, invalid data is now committed (Native Validation and Its Limits).
- Hydration mismatch discarding a subtree and re-rendering it, producing a visible flash on content that was already correct (Hydration Mismatch).
- The mitigation failing: disabling controls until hydration and then failing to enable them, because the enabling effect throws or never runs. The page is now permanently inert and looks deliberate.
- A long hydration task blocking a scroll or an animation that was already running, so the page visibly stutters at the moment it was supposed to become usable (Long Tasks).
- Hydration that runs before the data it needs, so the first client render differs from the markup and the correction is the user's first impression (Out-of-Order Responses).
- A click lands between paint and listener attachment. Nothing is queued and nothing is retried; the press is discarded unless the framework records and replays it.
- Hydration completes while an animation or scroll is in flight, and the long task interrupts it visibly (The Rendering Opportunity).
- A background refresh resolves during hydration and commits new data into a subtree that is still reconciling against server markup (Stale-While-Revalidate).
- Two chunks for one route arrive out of order, so a lazily-hydrated region becomes interactive before the region above it does — which is fine, unless the user reads top to bottom (Code Splitting).
- Hydration consumes the serialized state the server embedded, which makes that blob part of the trust boundary: anything in it is public, and anything the client trusts from it is attacker-inspectable (Server-Side Rendering).
- Client-side validation attached during hydration is not a control at any point, and it is especially not one during the interval when it does not exist. The server validates every submission regardless (Native Validation and Its Limits).
- A page that appears interactive before it is invites the double-submit that idempotency exists for — the user presses twice, and if the endpoint is not idempotent, the effect happens twice (Submission: Method, Encoding and Doing It Once).
- Rendering server markup and then hydrating over it means untrusted content passes through two renderers. Anything that escapes in one and not the other is an injection surface (Sanitization and Trusted HTML).
- Authorization-dependent controls in server markup are visible during the gap regardless of what hydration later decides to hide. Visibility is not a permission (Authorization-Aware UI).
- "Hydration is just attaching event listeners." In frameworks that rebuild the tree to find out where the listeners go, the walk usually costs more than the attachment.
- "The page is visible, so the work is done." Visible is a paint. Interactive is a listener plus a state container plus a main thread that is free to run them.
- "Server rendering plus hydration is strictly better than client rendering." It renders twice instead of once, ships the same bundle plus embedded state, and adds a gap that client rendering does not have. It buys early content, which is often worth it and is not free (Client-Side Rendering).
- "We can hydrate faster." You can hydrate less, and you can hydrate later, and you can ship less. The walk itself is roughly proportional to what you asked the framework to own.
- "Users do not click that early." They do, and the ones on slow devices click earliest, because the page has looked ready to them for longer.
Measuring it, and what changes in the field
- The gap itself: the interval between content painting and the page answering input. It is the number the strategy discussion is really about, and it is not visible in any single metric that only looks at paint (Interaction Responsiveness).
- The main-thread flame chart during load. Hydration has a characteristic signature — a long script block with no paint inside it, immediately after a large paint (A Mental Model of the Devtools).
- Long tasks concentrated before first interaction. That specific placement distinguishes hydration cost from ordinary application work (Long Tasks).
- Interactions that produced no visible response, collected from real users. This is the only measurement that catches discarded input, because it is invisible in a lab profile where nobody clicks early (Real User Monitoring).
- Bundle bytes for the route, since download is the first term in the gap and the easiest one to attribute (Bundle Analysis).
- On a low-end device the gap widens on every term at once — download, parse, execute and walk are all slower — while the paint that opened it does not move. The worse the device, the more misleading the early paint (The Real Cost of JavaScript).
- On a slow network the download term dominates and the page can sit looking complete for a long stretch, which is exactly when a user is most likely to start pressing things.
- On a large page the walk scales with node count, so a long list makes hydration expensive even though the list is not interactive (List Virtualization).
- On a repeat visit with a warm cache the download term disappears and the gap shrinks to parse plus execute plus walk — which is why the problem looks solved to whoever has loaded the page a hundred times.
- Early content, bought with a period in which the page is dishonest about its own state — and the earlier the content, the longer that period.
- A single component model shared between server and client, bought with a second full render of the tree and a markup contract that must hold (Hydration Mismatch).
- Yielding during hydration makes the page answer input sooner and finish hydrating later, which is usually the right trade and is not free (Yielding and Scheduling).
- Disabling controls until hydration is honest and looks slower. Some teams will not accept it, and that is a product decision worth making explicitly rather than by default.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe existence of an interval between paint and interactivity follows from the fact that listeners are attached by script that must first arrive and run, and it exists in every framework that renders on the server and attaches behaviour on the client.
- FRAMEWORK-SPECIFICThe size and shape of the cost varies enormously: React rebuilds the tree and reconciles against the DOM, Svelte and Solid compile to code that attaches to known nodes without a virtual tree, Angular and Vue sit between the two, and resumable approaches such as Qwik serialize listener state so the client can respond without a hydration pass at all.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering: a test that interacts only after the page has fully settled can never observe the gap. Asserting behaviour during hydration needs a test that acts as soon as content is visible, which is the moment a real user acts.