What Is Hydration? The Step That Makes Server-Rendered HTML Interactive

  • vuetelemetry
  • Guides
  • 7 min read

Server-rendered HTML is inert until hydration attaches Vue to it. What hydration does, why createSSRApp matters, the three documented causes of a hydration mismatch, and why a mismatch is a silent performance tax rather than a crash.

Server-side rendering gives you HTML that is complete before any JavaScript runs, which is why it helps first paint and crawlers. But that HTML is inert. Vue's own documentation puts it plainly: if you click the button, the number does not change, because the page is static until the framework takes over in the browser. Hydration is that takeover.

Vue describes the mechanism precisely. During hydration it creates the same Vue application that was run on the server, matches each component to the DOM nodes it should control, and attaches DOM event listeners. Nothing is rebuilt from scratch when it goes well: the markup is already there, and hydration claims it.

It is opt-in rather than automatic. Mounting a server-rendered app on the client requires createSSRApp rather than createApp, because that tells Vue the HTML was pre-rendered and that it should hydrate instead of mounting new DOM nodes. Reaching for the wrong one is a common way to render everything twice without noticing.

When the two versions disagree

A browser inspector panel showing a DOM tree of nested div and header elements. A hydration mismatch is a disagreement between this tree and what the client-side app expects.
A browser inspector panel showing a DOM tree of nested div and header elements. A hydration mismatch is a disagreement between this tree and what the client-side app expects.

All of this assumes the server output and the client expectation agree. When they do not, you get a hydration mismatch, which Vue defines as the DOM structure of the pre-rendered HTML not matching the expected output of the client-side app.

The documentation names three causes, and none of them is exotic.

The three documented causes

The first is invalid HTML nesting. If a template puts a div inside a p, the browser's own parser corrects the structure while reading the server HTML, and the corrected DOM no longer matches what the client expects. The framework did nothing wrong: the browser rewrote the page before Vue ever saw it.

  • Hydration recreates the server-run application in the browser, matches components to the existing DOM nodes and attaches event listeners.
  • It requires createSSRApp rather than createApp, which tells Vue to hydrate instead of mounting fresh nodes.
  • A hydration mismatch is when the pre-rendered DOM does not match what the client-side app expects.
  • Vue names three causes: invalid HTML nesting corrected by the browser parser, randomly generated values, and differing server and client time zones.
  • Vue recovers automatically so the app keeps working, but discarded and remounted nodes cost rendering performance.

The second is randomly generated values. The same application runs twice, once on the server and once in the browser, and a random value is not guaranteed to be identical across those two runs. Anything seeded at render time will diverge.

The third is time zones. When the server and the client sit in different zones, a date formatted during the server run and the same date formatted in the browser will not agree. This is the cause that survives longest in production, because it only appears for users in the wrong place.

Why it is a silent cost

What happens next explains why this bug is so easy to ignore. Vue attempts to recover automatically and adjusts the pre-rendered DOM to match client state, so the application generally keeps working. The cost is a rendering performance loss, because incorrect nodes are discarded and new ones are mounted in their place.

A mismatch is therefore rarely a crash. It is a silent tax: the server produced HTML, then part of that work was thrown away and redone in the browser. You paid for server-side rendering twice and kept the benefit once. That is exactly the kind of regression a functional test never catches, because the page still looks right.

A mismatch is therefore rarely a crash. It is a silent tax: the server produced HTML, then part of that work was thrown away and redone in the browser. You paid for server-side rendering twice and kept the benefit once. That is exactly the kind of regression a functional test never catches, because the page still looks right.

- vuetelemetry

The simpler answer for static pages

If the data needed to render a page is the same for every user, Vue points at a simpler answer than rendering per request: static site generation, also called pre-rendering, where the page is rendered once during the build. Hydration still applies to the result, but the per-request server cost disappears. Before optimising an SSR pipeline, it is worth asking whether the page needed to be dynamic at all.

Related stack