Hydration errors

Hydration errors

What hydration is, why a mismatch costs more than a console warning, and why the author of a layer or module owns the problem for every app that consumes it.

Hydration is the step where the browser takes the HTML that Nitro already rendered and turns it into a live Vue application without throwing that HTML away. The server ran your components once through renderToString; the client runs them a second time, but instead of creating DOM nodes it walks the existing DOM in step with the vnode tree it produces, claims each node, attaches event listeners and wires up reactivity. That only works if the client's first render produces exactly the tree the server produced. Every hydration error is, at bottom, the same bug: the component rendered differently in Node than in the browser.

Why a mismatch is not "just a warning"

In development Vue tells you loudly. In production it says nothing and repairs the page on the fly: text nodes get their content overwritten, mismatched elements are thrown away and re-created, extra server nodes are deleted. The app usually keeps working, which is exactly why these bugs survive into production. What you pay for instead:

  • Layout shift. A re-created subtree paints twice; if its size differs, everything below it moves (CLS).
  • A flash of the wrong content. The user sees the server's version for a few hundred milliseconds, then the client's.
  • Lost server-rendered content. If the client decides a node should not exist, the HTML the crawler and the LCP measurement saw is gone.
  • Wasted CPU on the main thread. Hydration is supposed to be cheaper than a full mount; recovery makes it a mount plus a diff, on the critical path to interactivity.
  • A page that quietly disagrees with its state, because Vue does not patch every kind of mismatch during hydration.

The senior framing is that hydration mismatches are a correctness problem with a performance symptom. Wrapping the offending subtree in <ClientOnly> hides the symptom and deletes server rendering for that subtree; the fix is almost always to give both renders the same inputs, which is what useState, useAsyncData, useCookie and useId exist for.

Why this is your problem as a layer or module author

An application developer who ships a mismatch breaks one page in one app. A layer author who ships a useBreakpoint() composable that reads window.innerWidth in setup, or a module that registers a <DatePicker> formatting new Date() in the template, breaks every page of every consuming app, and the consumers cannot fix it without forking. Worse, the warning appears in their console, pointing at their page, and the first hours of debugging are spent in the wrong repository.

So the responsibilities that fall on you are different from an app developer's: your components must render deterministically on both sides by construction, your composables must document what they return during SSR, browser-only components must be marked as such so consumers never need to know, head and body attributes must go through unhead rather than the DOM, and the playground you ship must run an E2E test that fails on any console message matching /Hydration/.

How this section fits together

Start with the mechanism: what renderToString and createSSRApp().mount() actually do, how Nuxt ships state so both renders agree, and what Vue does when they do not. Then the catalogue: eighteen concrete causes, what each looks like in the console, and the Nuxt-native fix for each. Then a repeatable diagnosis workflow that ends in an automated test rather than a manual check. Finally the rules a layer or module author follows so the previous three pages become someone else's reading.

Be able to say

Be able to say· In one minute: what is a hydration mismatch and why should a module author lose sleep over it?

"Hydration is Vue's client re-running the render and attaching itself to the server HTML instead of building new DOM, so it only works when both renders produce the same tree. A mismatch means they did not: a timestamp, a localStorage read, invalid HTML the browser 'corrected', a Teleport target that was not in the server output. In development Vue warns; in production it silently patches text and re-creates nodes, and the user pays with layout shift, a content flash and wasted main-thread time. As a module or layer author I care more than an app developer because anything I ship runs in every consumer's app, and the warning shows up in their console pointing at their page, so I make my components deterministic, document my composables' SSR behaviour, and ship an E2E test that fails on the word 'Hydration'."