Hydration completed but contains mismatches.💥
SSR (Server-Side Rendering) is one of the biggest advantages of modern frameworks—fast loading, SEO-friendliness, and strong initial render performance. But there’s a side effect: Hydration Mismatch.
If you’ve seen a warning like this in a Vue or Nuxt project, you’re not alone:
Hydration completed but contains mismatches.
This looks mysterious but it’s quite logical. In this post, we’ll unpack what “hydration” actually is, why “mismatch” happens, and how to prevent it.
What Is Hydration?
Hydration is the process of taking server-rendered HTML and rehydrating it with a Vue instance in the browser.When an SSR page loads, the flow is:
- Server Render: Nuxt generates HTML and sends it to the browser.
- Client Hydration: The browser receives the HTML, bootstraps the Vue app, and recreates the same virtual DOM tree.
- Vue compares the HTML and the virtual DOM—if they’re identical, hydration succeeds.
In short: SSR HTML ≠ Client render output → Hydration Mismatch.
The Most Common Causes
1) Time- or browser-dependent code
<p>{{ new Date().toLocaleTimeString() }}</p>
Server time could be 12:00 while the client is 12:01. The browser spots the difference → DOM mismatch. 😬✅ Fix: Move browser-only code into onMounted().
<script setup>
import { ref, onMounted } from 'vue'
const time = ref('')
onMounted(() => (time.value = new Date().toLocaleTimeString()))
</script>
2) Random values
<p>{{ Math.random() }}</p>
Server and client generate different numbers → never matches.✅ Fix: Generate the random value on the server and pass it as a prop, or generate it only on the client.
3) Accessing window or document
There’s nowindow/document on the server.const width = window.innerWidth // ❌ SSR crash
Run such code only on the client.✅ Fix:
if (process.client) {
const width = window.innerWidth
}
Or with the Composition API:
onMounted(() => {
console.log(window.innerWidth)
})
4) Conditional render differences
<div v-if="isClient">Client Render</div>
If isClient differs between server and client, Vue renders different DOM structures.✅ Fix: Keep the same initial state across SSR and client. Use useState() if needed.
5) Async data inconsistencies
Data fetched viauseAsyncData should be ready during SSR. If the client refetches instead of using SSR data, the DOM can differ.✅ Fix: Always provide a unique key to each useAsyncData call.
await useAsyncData('posts', () => $fetch('/api/posts'))
Otherwise Nuxt may refetch instead of using cache, causing content drift.
Understanding the Hydration Flow
Vue proceeds as follows:- HTML is generated during SSR.
- The Vue app boots on the client.
- Vue compares SSR HTML with its client render.
- On difference, it logs a
hydration mismatchwarning. - Vue then rebuilds parts of the DOM (even if you don’t see a difference), incurring a perf cost.
Helpful Tools for Diagnosis
- Detailed
console.warnmessages: Vue often points to the problematic node. nuxt dev --debugmode: Highlights hydration diffs line-by-line.- Vue Devtools: Inspect differences between SSR HTML and client render.
Fix Matrix
| Problem | Solution |
|---|---|
| Time/date-based rendering | Compute in onMounted |
| Random values | Generate client-only or inject from server |
| window/document usage | Use process.client or onMounted |
| API data inconsistency | Unique keys for useAsyncData |
| Conditional render drift | Start from the same SSR/client state |
Bonus: isHydrating & onHydrated
In Nuxt 3, access hydration status viauseNuxtApp().const nuxtApp = useNuxtApp()
nuxtApp.hook('app:hydrated', () => {
console.log('Hydration completed!')
})
This event fires when Vue finishes matching DOM—handy for performance metrics.Hydration vs Suspense
With<Suspense>, async children can diverge between SSR and client: a fallback renders on SSR, while real content appears on the client.✅ Fix: Either set suspensible: false on async components under Suspense, or guard with v-if="hydrated".
Conclusion
Hydration mismatches look scary but always boil down to the same cause: the server and client produced different DOM. The fix is predictable: run browser-specific logic only on the client.In short:
window,document,Math.random(),Date()= handle with care.- Keep SSR and client state aligned.
onMountedandprocess.clientare your best friends.