Hydration & Mismatches

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.

4 min read Level 3/5 #nuxt#hydration
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() or Date.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.

ClientOnly →