Hydration Mismatch
The client renders something different from what the server sent. The framework cannot quietly pick a winner, because it does not know which one is right.
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.
Why does the client disagree with the server-rendered markup, and why can the framework not just fix it?
Someone loads a page that looked correct, and watches part of it flicker, jump, or replace itself a moment later — or reports a control that never worked.
The same components run in both places, so the output is the same in both places. If it is not, the framework should notice the difference and patch the DOM to match the client, since the client is the one that has to keep rendering.
The two environments are genuinely different. The server has no window, no document, no localStorage, no viewport, no user timezone and no browser extensions; the client has all of them, and any code that reads one during render produces different output (The Browser Is a Runtime).
- The two environments are genuinely different. The server has no
window, nodocument, nolocalStorage, no viewport, no user timezone and no browser extensions; the client has all of them, and any code that reads one during render produces different output (The Browser Is a Runtime). - They also run at different moments. A timestamp, a relative time string, a random id or an expiring token rendered on the server is by definition older than the one the client computes (Timezones and Locale Formatting).
- Patching to match the client is not obviously right. If the client is wrong — it read a stale cache, or it has not received the data yet — patching replaces correct content with incorrect content, and the user watched it happen (Server State Is Not Your State).
- A patch is not cheap either. Correcting a subtree means discarding DOM the browser already styled, laid out and painted, and doing all of that again on content that was on screen (The Cost of a Change).
- The HTML the client finds may not be the HTML the server sent. The parser fixes invalid nesting, and extensions inject nodes — so the tree the framework compares against was already modified by something neither environment controls (Tree Construction).
- Some mismatches are not visible at all. A mismatch on an attribute can leave the paint correct and the behaviour wrong, which is the version that survives review and reaches production (Hydration).
What is actually happening
In the browser, not in the framework.
- Hydration is a comparison. The client renders the component tree and walks the existing DOM in step with it, expecting the structures to correspond. A mismatch is that correspondence failing (Hydration).
- The framework's options at that point are all bad: trust the markup and attach anyway, leaving the DOM disagreeing with the state that now owns it; discard the subtree and rebuild it, throwing away the paint; or refuse and fall back to a full client render, throwing away all of it.
- It cannot resolve the disagreement on the merits, because it has no idea which value is correct.
1:04 PMand1:05 PMare both plausible; so are two different currency formats and two different randomly generated ids. - Structural mismatches are the severe ones: a different element type or a different number of children breaks the correspondence for everything after it, so the framework usually discards the whole subtree rather than trying to realign (Reconciliation and Keys).
- Text and attribute mismatches are narrower and are often patched in place, sometimes with a warning and sometimes silently, depending on the framework and the build mode.
- The invalid-nesting case is particularly confusing because your code is symmetric and the DOM is not. A
divinside ap, or atroutside atbody, is relocated by the HTML parser before any framework sees it, so the client's expected structure and the actual DOM differ for a reason that is not in either render (Document Structure and Reading Order).
What this makes the browser do
And which of it is avoidable.
- A second construction of any discarded subtree, including every node the server already produced (What a Mutation Costs).
- Style resolution and layout over the replaced region, and paint over whatever area it covers (The Rendering Pipeline).
- In the worst case, discarding the entire document body and re-rendering from the root — which converts a server-rendered page into a client-rendered one with extra steps and a visible flash (Client-Side Rendering).
- Avoidable: all of it, by keeping render output a pure function of data both environments have.
- Unavoidable: the correction for genuinely client-only values. The right move is not to render them on the server at all, so there is nothing to correct (Hydration).
The causes, and what each one looks like
Mismatches are not a grab bag. They come from a short list of inputs that exist in one environment and not the other, plus one that comes from neither: the browser mutating the HTML before hydration. Recognising which of these you are looking at is most of the fix, because the response is different for each.
The table below is worth reading as a diagnostic. The symptom column is what gets reported; the cause column is what to look for; the response column is the actual fix, which in almost every row is "do not render this on the server" rather than "make the client agree".
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| A timestamp, a relative time, or anything derived from the current instant | A time that flickers to a different value, or a "2 minutes ago" that jumps on load | The server rendered at one moment and the client at a later one. There is no arrangement under which these agree | Serialize the absolute instant, render a stable representation on the server, and compute the relative form in an effect after mount (Timezones and Locale Formatting) |
| A random value, a generated id, or anything derived from entropy | An attribute mismatch warning naming an id or a for, or a label that stops being associated with its input | Two independent calls produced two different values, and the association between label and control was built from one of them | Use the framework's stable-id facility, or pass the id in as a prop from a single source (What a Component Owes Its Caller) |
A browser-only API read during render — window.innerWidth, matchMedia, localStorage | A crash on the server, or a component that renders one variant on the server and another on the client | The server has no viewport, no media queries and no storage, so the code either throws or takes the fallback branch | Read it in an effect and render the server-safe variant first; for layout variation, prefer CSS that both environments emit identically (Container Queries) |
| Locale, timezone or currency formatting using the ambient environment | Reports of dates and numbers changing shape on load, only from some regions | The server formatted with its own locale and timezone and the client with the user's | Choose the locale and timezone explicitly and pass them through, so the same inputs produce the same string in both places (Internationalization) |
| Server and client starting from different data | A value visibly corrected shortly after load, sometimes to something older | A client-side cache, a persisted store or a second fetch seeded the client render with different inputs than the server had | Hydrate the client cache from the serialized server state rather than from storage, and make the precedence explicit (Persistent Client State) |
| The browser modifying the HTML before hydration | A structural mismatch in a component whose code is obviously symmetric | The parser repaired invalid nesting, or an extension injected nodes. Neither render produced the DOM being compared | Validate the served markup for nesting errors; for extensions, keep the hydration root away from regions they commonly touch and accept that some of this is not yours to fix (Document Structure and Reading Order) |
Rendering client-only values without lying
Almost every fix in the table has the same shape: stop asking the server to render something it cannot know, and render it after mount instead. That sounds like a workaround and is actually the correct model — the server genuinely does not know what time it is where the user is, and pretending otherwise is what created the disagreement.
The version below is deliberately framework-neutral in spirit even though the syntax is React-flavoured: render a value both environments can produce, then upgrade it once the client is running. The important detail is the third one, which is easy to skip: reserve the space, or the fix for a flicker becomes a shift.
1// Mismatches every time: the server and the client render at different moments,2// and the client also has a timezone the server does not know.3function PostedAt({ iso }: { iso: string }) {4 return <time dateTime={iso}>{relativeTime(new Date(iso))}</time>5}6 7// Stable in both environments, upgraded once the client is running.8function PostedAt({ iso }: { iso: string }) {9 // Both renders produce this exact string: no ambient locale, no ambient zone,10 // no reading of "now" during render.11 const absolute = formatFixed(iso, { locale: 'en-GB', timeZone: 'UTC' })12 const [label, setLabel] = useState(absolute)13 14 useEffect(() => {15 // Runs only on the client, after hydration has already matched.16 setLabel(relativeTime(new Date(iso)))17 }, [iso])18 19 return (20 // The width is reserved so the upgrade does not move anything below it.21 <time dateTime={iso} style={{ display: 'inline-block', minWidth: '9ch' }}>22 {label}23 </time>24 )25}The server output is a real, readable, indexable timestamp rather than a placeholder — so a client that never runs JavaScript still gets a correct answer, and a client that does gets a better one without a disagreement in between.
What a correction costs the browser
It is worth being precise about why "just patch it" is not free, because the answer decides how much effort a given mismatch deserves. A text correction is cheap and a structural rebuild is not, and the difference is which pipeline stages the change invalidates on content that has already been painted.
The row that should worry you is the last one. A mismatch severe enough to make the framework abandon server markup for the document root turns a server-rendered page into a client-rendered page, mid-load, in front of the user — with the paint already spent and nothing to show for it.
| Change | style | layout | paint | composite | Why |
|---|---|---|---|---|---|
| Hydration matches: listeners attached, no DOM change | no | no | no | no | The intended path. The DOM is untouched, so none of the rendering stages run again — the cost is entirely the main-thread walk that produced the match. |
| A text node corrected in place | no | maybe | yes | yes | The text must be repainted. Layout runs again only if the new string changes the size of its box, which is why reserving width turns a maybe into a no. |
| An attribute corrected (`aria-expanded`, `data-state`) | maybe | maybe | maybe | maybe | Depends entirely on whether any selector matches on that attribute. If none does, this is a pure accessibility-tree change with no visual cost at all — and no visual signal either, which is why this class hides so well. |
| A class corrected on a container | yes | maybe | yes | yes | Style must be recomputed for the element and anything inheriting from it. Whether layout follows depends on which properties the class changes (The Cost of a Change). |
| A subtree discarded and rebuilt | yes | yes | yes | yes | Every node is constructed again, styled again, laid out again and painted again — over a region the browser had already finished. This is the visible flash users report. |
| Fallback to a full client render of the root | yes | yes | yes | yes | The entire document body is replaced. The server render is now pure overhead: it cost a per-request render, delayed the first byte, and its output was thrown away (Client-Side Rendering). |
caveat Every maybe here depends on the rest of the page: whether a selector matches the changed attribute, whether the corrected text changes its box, and whether the affected element sits in a containing block that isolates layout from the rest of the document (CSS Containment).
How to build it
Most important first.
- Make render a pure function of data that both environments have. Anything else — time, randomness, viewport, storage, locale — is an input the server does not possess and must not be read during render (Derived State).
- Render client-only values in an effect after mount, not during render. The server emits a stable placeholder, the client fills it in, and the two never disagree because they were never asked to agree (State Synchronization).
- Send the raw value and format it consistently. A timestamp serialized as an absolute instant, formatted with an explicitly chosen locale and timezone, produces the same string in both places — formatting with the ambient environment does not (Timezones and Locale Formatting).
- Pass ids and random values in as props from a single source, or use the framework's stable-id facility, rather than generating them during render (What a Component Owes Its Caller).
- Validate your markup nesting. Structural mismatches caused by the parser repairing invalid HTML are invisible in the component source and obvious in the served document (Document Structure and Reading Order).
- For content that legitimately differs — a personalized greeting on an otherwise cacheable page — render the shared version on the server and treat the personal version as a deliberate, announced, space-reserved update after paint (Visual Stability).
- Never suppress the warning globally. Suppressing it for one known-unstable text node is a scoped decision; switching it off across the app deletes the only signal you have (Frontend Error Tracking).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- A discarded and rebuilt subtree replaces nodes in the accessibility tree, which can move or destroy focus if the user had already tabbed into it — during hydration they very often have (Focus Management).
- Content a screen reader has already announced can be replaced by different content with no announcement, so the user's model of the page and the page itself silently diverge (Live Regions and Announcement).
- An attribute mismatch is an accessibility bug more often than a visual one, because
aria-expanded,aria-selectedandaria-liveare attributes. A control whose visible state and announced state disagree is worse than one that is simply broken (The Rules of ARIA). - A mismatch that falls back to full client rendering deletes the content from the document, which is the one thing server rendering was providing to assistive technology in the first place (The Accessibility Tree).
- A correction that changes a label, a heading or a landmark changes how the page can be navigated, and it does so after someone may have built a mental map of it (Semantics Before ARIA).
What can go wrong
- A visible flash: correct content painted early, discarded, and re-rendered, so the page appears to stutter at exactly the moment it should have settled.
- A silent behavioural break: the DOM keeps the server's value while the framework's state holds the client's, so the next update writes something that makes no sense in context.
- A whole-page fallback to client rendering, which removes the benefit of server rendering while keeping all of its costs (Server-Side Rendering).
- An intermittent mismatch that only appears for users in certain timezones, locales or extensions, which no local reproduction will find (Real User Monitoring).
- The mitigation failing: rendering a placeholder on the server and filling it in on the client without reserving space, so the fix trades a flash for a layout shift (Visual Stability).
- Suppressing the warning to make the console quiet, then losing the ability to detect the next mismatch — including the one that breaks behaviour rather than pixels.
- The server renders at one instant and the client at a later one, so any time-derived output disagrees by construction — the most common mismatch there is.
- Data changes between the server render and the client's first render, so the client is correcting content that was accurate when it was produced (Stale-While-Revalidate).
- An extension injects nodes between parse and hydration, so the DOM the framework compares against is not the DOM that arrived.
- A client-side cache is rehydrated from storage before the first render, seeding a value the server did not have (Persistent Client State).
- The mismatch itself is not a vulnerability, but the usual cause of a personalization mismatch is: a per-user response served from a shared cache, so the markup belongs to somebody else and the client is correcting it (Browser HTTP Caching).
- A page that falls back to full client rendering exposes whatever the client render exposes, including any state the server had embedded — the fallback path is not less public than the normal one (Server-Side Rendering).
- Suppressed mismatch warnings hide the signal that a cached response is being served to the wrong user, which is exactly the failure you most want to be noisy (Frontend Error Tracking).
- Rendering untrusted content through two different renderers means two escaping implementations must agree. Where they do not, one of them is a sink (Sanitization and Trusted HTML, Cross-Site Scripting).
- "The framework should just use the client's version." Sometimes the client is the wrong one, and in every case the patch costs a rebuild of content that was already correct on screen.
- "It is only a warning." A structural mismatch can discard the whole subtree, and an attribute mismatch can leave announced state disagreeing with visible state. Neither is cosmetic.
- "It works locally, so it is fine." Locally you have one timezone, one locale, no extensions and a warm cache. The mismatch classes that matter are the ones that need none of those to be true.
- "Adding a random key fixes it." It stops the comparison from matching anything, which converts a targeted correction into a full rebuild of that subtree (Reconciliation and Keys).
- "We can generate ids during render as long as they are unique." Unique is not the requirement. Identical across two renders in two environments is (What a Component Owes Its Caller).
Measuring it, and what changes in the field
- The framework's development-mode warning, read rather than dismissed. It names the component and usually the differing text, which is most of the diagnosis (A Method for Frontend Bugs).
- View-source against the elements panel. The first is what the server sent, the second is what the client ended up with, and the diff between them is the mismatch (A Mental Model of the Devtools).
- A performance trace: a mismatch shows as a large layout and paint immediately after hydration, on a region that had already been painted (Debugging Rendering and Jank).
- Content movement after paint in field data, which catches mismatches that only occur in timezones, locales or extension configurations you do not have (Visual Stability).
- Production error reporting for hydration warnings specifically. They are usually suppressed in production builds, so this needs a deliberate decision to collect them (Frontend Error Tracking).
- In a different timezone or locale, time and number formatting diverge for users you cannot reproduce locally — this is the single most common source of mismatch reports (Internationalization).
- With a browser extension installed, the DOM contains nodes neither renderer produced. Password managers, translators and accessibility tools all mutate documents (The Multi-Process Browser).
- On a slow device the flash is longer and therefore more visible, because the corrected region is repainted well after it was first shown.
- On a page with a stale cached response the mismatch is not a bug in your render at all — it is a signal that the caching layer is serving something it should not (Browser HTTP Caching).
- In a development build the warnings are loud; in a production build they are usually silent, which means the same defect is diagnosable in one environment and invisible in the other.
- Keeping render pure across two environments constrains what components may do: no reading the viewport during render, no ambient locale, no ad-hoc randomness. That is a real restriction, and it is the price of rendering the tree twice.
- Deferring client-only values to an effect guarantees correctness and guarantees a second paint for those values, which must have space reserved for it.
- Suppressing a warning for one genuinely unstable node keeps the signal useful elsewhere and hides a real difference at that node. Scope it as narrowly as the framework allows.
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.
- GENERALAny strategy that renders markup in one environment and re-derives it in another can disagree, so the cause list holds for every framework with server rendering — what differs is only how loudly and how early the disagreement surfaces.
- FRAMEWORK-SPECIFICThe response to a mismatch is not standardised: React warns and may discard the subtree or fall back to a client render of the root, Vue patches and warns in development while staying silent in production, and compiled frameworks such as Svelte and Solid detect fewer classes of mismatch because they attach to known nodes rather than reconciling a whole tree.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering: hydration mismatches are environment-dependent by definition, so a test suite that runs in one timezone with one locale and no extensions proves very little. Running the same render assertions under a second timezone and locale finds most of this class before users do.