Interviewers open with "what is hydration?" not to hear a definition but to find out whether you know what the client actually does with the server HTML, and therefore why the fixes on the following pages work. A mid-level answer says "Vue attaches event listeners". A senior answer explains the parallel walk, where the inputs come from, and what recovery costs.
createSSRApp and calls Vue's renderToString(app, ssrContext) from vue/server-renderer. Nuxt wraps the result with the head tags unhead collected and with the payload: useState and useAsyncData/useFetch results serialised (via devalue) into a <script type="application/json" id="__NUXT_DATA__"> block.createSSRApp(App).mount('#__nuxt'). Because the app was created with createSSRApp, mount hydrates: Vue runs the first render, and for every vnode it produces it steps to the next existing DOM node, claims it (vnode.el), attaches listeners and moves on. It creates nothing. Hydration succeeds only if the client's first render yields the same tree the server rendered.useState refs and useAsyncData results already hold the server's values on the client; useCookie reads the request's Cookie header on the server and document.cookie in the browser, so both see the same value; unhead renders head tags on the server and hydrates them on the client. Anything outside these channels (Date.now(), window, a raw $fetch) is an input only one side has.Hydration node mismatch, Hydration text mismatch, Hydration children mismatch, Hydration attribute/class/style mismatch, each with "rendered on server" against "expected on client" plus a component trace, and finally Hydration completed but contains mismatches.__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ (Vue 3.4, compile-time flag, default false) makes production builds log the same details as development, at some bundle cost. Vue's docs set it through Vite define; Nuxt's debug option does the same thing for you: debug: { hydration: true } (or debug: true, which expands to every debug flag) sets the define in the Vite builder.data-allow-mismatch (Vue 3.5) declares an expected mismatch on one element. Values: text, children (direct children only), class, style, attribute; no value allows every kind. It suppresses the warning; the recovery still happens.useId() (Vue 3.5, import from vue) returns an id that is unique per app instance and identical on server and client, the correct source for generated id / aria-* / for attributes. Prefix apps sharing one page with app.config.idPrefix; never call it inside computed. Nuxt 4 no longer documents a useId of its own; use Vue's.Stripped of Nuxt's plumbing, the two entries look like this:
// server (inside Nitro's render handler)
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const app = createSSRApp(App)
const html = await renderToString(app, ssrContext) // components run once in Node
// ...Nuxt adds unhead tags + the devalue payload and sends the document
// client (app entry)
const app = createSSRApp(App)
app.mount('#__nuxt') // components run again; Vue walks the existing DOM instead of creating it
Turning production details on for one environment only, so a staging build tells you what production would not:
export default defineNuxtConfig({
$env: {
staging: {
// sets __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ = true in the Vite build
debug: { hydration: true },
},
},
})
Build it with nuxt build --envName staging. The equivalent without the Nuxt option is vite: { define: { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true' } }.
A component that uses both Vue 3.5 tools, one to prevent a mismatch and one to declare an acceptable one:
<script setup lang="ts">
import { useId } from 'vue'
defineProps<{ iso: string }>()
const labelId = useId() // identical on server and client
</script>
<template>
<p>
<span :id="labelId">Published</span>
<!-- Node and the browser format dates with different locale/timezone; the flash is acceptable here -->
<time :datetime="iso" :aria-labelledby="labelId" data-allow-mismatch="text">
{{ new Date(iso).toLocaleString() }}
</time>
</p>
</template>
During hydration Vue patches text and replaces wrong nodes, but for a plain attribute, class or style mismatch it only logs. The element keeps the server's value until that binding is next updated by a reactive change. A theme class computed from localStorage therefore does not just flash, it stays wrong while your state says otherwise, and no error ever tells the user.
It silences the warning for one element and keeps the recovery (the text is still overwritten, the flash still happens). Use it where a mismatch is inherent, such as a timestamp formatted in the user's locale. Sprinkling it over components to get a clean console is how a team loses the signal for real bugs.
useCookie(name, { secure: true }) on a non-HTTPS origin: the browser refuses to send the cookie back, the server renders the default while the client reads the value it set locally, and the mismatch looks like a state bug. The Nuxt docs call this out; test cookie-driven UI over HTTPS or without secure in local dev.
debug accepts true or a granular object; the schema resolves true to { templates, modules, watchers, hooks, nitro, router, hydration, perf } and the Vite define block derives __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ from debug.hydration. Re-check the option name on the config reference the week before; the study guide also claims Nuxt DevTools surfaces hydration details, which is not documented on the DevTools features page.
nuxt dev playground, render {{ Date.now() }} in app/pages/index.vue. Read the full warning: which mismatch type is it, what did the server render, what did the client expect, which component does the trace name?nuxt build && node .output/server/index.mjs. Confirm the console is silent and the value still flips after load (that is the recovery).debug: { hydration: true } to nuxt.config.ts, rebuild, and confirm the production bundle now logs the details. Compare the client bundle size with and without the flag.Date.now() with useState('now', () => Date.now()) and watch the warning disappear; open view-source and find the value inside #__NUXT_DATA__.toLocaleString(), add data-allow-mismatch="text", and notice the console is clean while the text still flashes."The server renders the component tree to HTML with renderToString; the client calls createSSRApp().mount(), which runs the same render but walks the existing DOM and claims nodes instead of creating them. A mismatch means the client's first render produced a different tree, because some input existed on only one side: a timestamp, a browser API, data fetched outside the payload, or HTML the browser corrected. In development Vue names the element and prints server versus client. In production it recovers silently: text is patched, wrong nodes are re-created, attribute mismatches are not even repaired. The user pays in layout shift, a content flash and a partial re-mount on the main thread. Nuxt's answer is to feed both renders the same inputs through useState, useAsyncData and useCookie, and Vue 3.5 adds useId for stable ids and data-allow-mismatch for the rare mismatch you intend."
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.
Catalogue of causes
The eighteen ways a Nuxt app renders differently in Node and in the browser, what each one looks like in the console, and the Nuxt-native fix for each.