"A consumer reports hydration warnings on their dashboard, you cannot reproduce it. What do you do?" is the scenario question for this section. The interviewer wants a method, not a guess: something that narrows the search space step by step and ends with a regression test rather than a fix and a hope. This is that method.
node, text, children, attribute, class, style), prints what was rendered on the server against what the client expected, and attaches a component trace. Click through to the component; half the catalogue is identifiable from this message alone.curl -s http://localhost:3000/page > server.html (or view-source:). In the browser, after load, run copy(document.documentElement.outerHTML) and paste into client.html; format both with npx prettier --parser html and diff. The post-hydration DOM is the repaired version, which is the point: the diff shows exactly what Vue changed. A tab with JavaScript disabled shows the raw server DOM if you need it.debug: { hydration: true } (or vite: { define: { __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true' } }) and reproduce there. Do not ship it: the flag adds bundle size and logs to every user.TZ=Europe/Prague LANG=cs_CZ.UTF-8 node .output/server/index.mjs), auth state (logged in against logged out), and cached HTML (check age and cache-status response headers, and whether it started right after a deploy).<ClientOnly> until the warning disappears, then look at what you wrapped. Remove the wrappers when you have the culprit; they are a probe, not a fix.#__NUXT_DATA__ script; the console has useNuxtApp().payload.data and .state. Ask: is the data the client renders actually there? Missing means it was fetched outside the payload (cause 10) or with server: false; present but different means a transform/pick difference or a key collision (cause 9).createPage that collects console messages and fails on anything matching /Hydration/. Run it against every fixture. This is the test a layer or module ships with its playground.The test, with @nuxt/test-utils driving Playwright from Vitest:
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { createPage, setup, url } from '@nuxt/test-utils/e2e'
const routes = ['/', '/dashboard', '/settings/profile']
describe('hydration', async () => {
await setup({
rootDir: fileURLToPath(new URL('../../playground', import.meta.url)),
browser: true,
// production build (what users get), but with Vue's mismatch details compiled in
nuxtConfig: { debug: { hydration: true } },
})
it.each(routes)('%s hydrates without warnings', async (route) => {
// no path here: the console listener must exist before navigation starts
const page = await createPage()
const problems: string[] = []
page.on('console', (msg) => {
if (/Hydration/.test(msg.text())) problems.push(msg.text())
})
page.on('pageerror', (error) => problems.push(error.message))
// waits for window.useNuxtApp().isHydrating === false, not just for `load`
await page.goto(url(route), { waitUntil: 'hydration' })
expect(problems).toEqual([])
await page.close()
})
})
Why each line is there: browser: true launches Playwright (install playwright-core and a browser binary with npx playwright install chromium); nuxtConfig overrides the fixture's config for this run, so the production bundle logs mismatches without the flag ever reaching a real deploy; createPage() without a path returns a page that has not navigated yet, so no early warning is lost; waitUntil: 'hydration' is test-utils' extension of Playwright's goto, which polls window.useNuxtApp?.().isHydrating === false, because load fires before Vue has finished walking the DOM.
The same check with the Playwright test runner, if the team prefers it over Vitest:
import { expect, test } from '@nuxt/test-utils/playwright'
test('home hydrates cleanly', async ({ page, goto }) => {
const problems: string[] = []
page.on('console', msg => /Hydration/.test(msg.text()) && problems.push(msg.text()))
await goto('/', { waitUntil: 'hydration' })
expect(problems).toEqual([])
})
Run the Vitest version in CI with a small matrix: the default environment plus one job with TZ=Europe/Prague and a non-English LANG, which is how cause 2 is caught before a user in Prague reports it.
setup() builds and serves a production bundle by default. Without debug.hydration Vue never logs a mismatch there, so the console stays clean and the test passes with a broken page. Either run the fixture in dev mode (dev: true) or, better, keep the production build and switch the details on through nuxtConfig.
Passing a path to createPage performs goto immediately, and hydration warnings are emitted during that navigation. Attach page.on('console') first, then navigate yourself.
Every <ClientOnly> you leave in place deletes server rendering for that subtree, and its default slot's CSS is no longer inlined in the initial HTML. Find the cause with it, then take it out and apply the fix from the catalogue.
setup() options used here (rootDir, browser, nuxtConfig, dev) and the waitUntil: 'hydration' extension are current in @nuxt/test-utils; the goto fixture of the Playwright integration accepts the same option. Re-check the option names against the testing page shortly before the interview.
{{ Date.now() }} to a page and passes when you remove it.localStorage that dev-mode HMR happens to mask), confirm the plain production build is silent, then find it with debug: { hydration: true }.curl the page, copy outerHTML, prettier both, diff. Identify the changed nodes and match them to the warning."First I accept that production is silent by design: Vue compiles the warnings out, so I build a staging variant with debug: { hydration: true }, which sets Vue's __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ flag, and reproduce there. Then I diff the server HTML from curl against the browser DOM after load to see exactly what Vue repaired. If only some users hit it, I reproduce with their environment: timezone and locale on the server process, logged in versus out, a clean browser profile to rule out extensions, and the cache headers to rule out stale or personalised HTML. If I still cannot see it, I bisect with <ClientOnly> and check the payload in DevTools to see whether the data the client renders was ever there. The fix ends with an E2E test that collects console messages after waitUntil: 'hydration' and fails on the word Hydration, so it cannot come back."
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.
Author responsibilities
The rules a layer or module author follows so that nothing they ship can cause a mismatch in a consumer's app, and how they prove it in the playground.