When Server and Client HTML Disagree
Hydration & Mismatches
A hydration mismatch happens when the client's first render produces different HTML than the server's. The browser warns, and Vue may discard the SSR HTML.
What you'll learn
- Recognize a hydration mismatch warning in the console
- Fix mismatches by deferring browser-only code to onMounted
- Use ClientOnly for cases that cannot be unified
Hydration is the process where Vue takes the static HTML the server sent and “wires it up” with event listeners and reactivity. It only works if the client’s first render produces the same DOM as the server’s.
What a Mismatch Looks Like
In the browser console:
[Vue warn] Hydration node mismatch:
- rendered on server:
2026-05-13T08:12:31.488Z- expected on client:
2026-05-13T08:12:33.102Z
The two renders disagreed, so Vue threw away the SSR HTML for that subtree and re-rendered from scratch — defeating the purpose of SSR.
Common Causes
new Date()orDate.now()directly in templates.Math.random()in templates.- Browser-only APIs like
window,localStorage,navigator. - Different timezones between server and client.
Fix 1 — Defer to onMounted
<script setup lang="ts">
const now = ref<Date | null>(null)
onMounted(() => {
now.value = new Date()
})
</script>
<template>
<div v-if="now">{{ now.toLocaleString() }}</div>
</template> onMounted runs only in the browser, after hydration. The server sees now === null and renders
nothing; the client then fills it in.
Fix 2 — process.client Guards
if (process.client) {
const stored = localStorage.getItem('theme')
// ...
} Use sparingly — overusing client-only logic erodes the SSR benefit.
Fix 3 — ClientOnly
For widgets that fundamentally can’t run on the server (charts, maps, third-party embeds), wrap
them. The next lesson covers <ClientOnly> in detail.