07 / 09

为什么会出现 Hydration Error?

分清 SSR Error 与 Hydration Mismatch,掌握定位方法、最常见成因和修复代码。

为什么会出现 Hydration Error?

学习目标

理解什么是 SSR Error、什么是 Hydration Error、两者有什么区别,以及怎么定位和修复。

学完本章,你应该能够:

  • 看到报错信息,判断它发生在服务器还是浏览器
  • 用控制台和 DevTools 找到不一致的组件
  • 说出最常见的 5 类成因并给出修复代码
  • 在需要时使用 onMounted<ClientOnly> 修复

一、这个知识解决什么问题?

上一章我们已经知道 SSR → 显示 HTML → Hydration 这条流程(Hydration 为什么不是重新渲染)。

那为什么有时候页面不报错,却会出现:

Hydration completed but contains mismatches.

或者:

Hydration node mismatch

很多人第一次遇到时,连"这个错是谁报的"都不清楚。本章的目标是:先能分清错误发生在哪个阶段,再谈修复。(以下描述基于 Nuxt 3.x 与 Vue 3 的默认行为,Nuxt 4 机制相同。)


二、一起推导

案例一:SSR Error

const width = window.innerWidth

服务器有没有 window没有。 所以服务器直接报错 ReferenceError,连 HTML 都生成不了:

SSR → window → ReferenceError → HTML 都没有返回

Hydration 根本还没有开始,所以这不是 Hydration Error,而是 SSR Error

案例二:真正的 Hydration Error

const width = ref(0)

if (import.meta.client) {
  width.value = window.innerWidth
}
  • 服务器width = 0,生成 <div>0</div>
  • 浏览器:执行 window.innerWidth,得到 1920
  • Vue 想接管:服务器 0 ≠ 浏览器 1920

Vue 在 Hydration 时检测到不一致,报 Hydration Mismatch。这才是真正的 Hydration Error

怎么定位是哪一行的问题?

Vue 在开发模式下会把不一致信息打印到控制台,通常包含组件名;打开 Vue Devtools 找到提示的组件,再把「查看网页源代码」里的 HTML 和 DevTools 里实际的 DOM 对比,不一样的地方就是问题点。

一个快速排查顺序:

1. 页面能不能打开? 不能 → 多半是 SSR Error(看服务器日志)
2. 能打开但控制台有 Hydration 警告? → 对比 SSR HTML 与浏览器 DOM
3. 找到了差异 → 看差异属于哪一类成因(下一节)

如果你对"副作用发生在哪个阶段"这个概念还不熟,可参考 理解与掌控副作用(Vue 3 和 React)


三、SSR Error 和 Hydration Error 的区别

SSR Error Hydration Error
发生时间 服务器生成 HTML 时 浏览器开始接管 HTML 时
现象 服务器直接崩溃(ReferenceError) 服务器与浏览器结果不一致
HTML 返回了吗 ❌ 没有 ✅ 有
哪里能看到 服务器日志 浏览器控制台
典型例子 直接访问 window 服务器输出 0,浏览器计算出 1920

四、最常见的 5 类成因和修复

1. 浏览器专属 API 在 setup 顶层被读取

<script setup>
const width = window.innerWidth // 服务器没有 window
</script>

修复一:默认值 + onMounted 里更新

<script setup>
const width = ref(0)
onMounted(() => {
  width.value = window.innerWidth
})
</script>

修复二:首屏结构依赖该值时,用 <ClientOnly> 包裹,服务端输出占位内容

<template>
  <ClientOnly>
    <ResponsivePanel />
    <template #fallback><div>加载中</div></template>
  </ClientOnly>
</template>

2. 时间、随机数这类"每次都不同"的值

<script setup>
const now = new Date().toLocaleString() // 服务器和浏览器结果不同
</script>

首屏渲染的时间、随机 ID 会让两端永远对不上。要么把这类值推迟到 onMounted 后更新,要么用 <ClientOnly>

3. localStorage / sessionStorage

服务器端根本没有 localStorage,直接读会变成 SSR Error;即使写在 import.meta.client 判断里,两端值也可能不一致:

const theme = ref(localStorage.getItem('theme')) // SSR 阶段直接报错

正确做法是默认值 + onMounted 读取,配合一个"加载后再显示"的状态。

4. 不稳定的列表 key

<template>
  <div v-for="(item, index) in list" :key="index">

如果列表内容依赖浏览器环境(比如按窗口宽度过滤、排序),服务端和客户端的顺序不同,key 就会对不上。key 必须来自数据本身(item.id),不能来自"随环境变化的顺序"。

5. 非法 HTML 嵌套

<table><div>...</div></table>
<p><div>...</div></p>

浏览器解析器会自动修正非法嵌套,导致「服务器生成的字符串」和「浏览器实际解析出的 DOM」结构不同,Hydration 必然 mismatch。修复方法是改成合法嵌套,或用 <ClientOnly> 隔离。

另外还有一种"假阳性":浏览器扩展改写了页面 DOM。如果问题只在某个浏览器或环境出现,先换无痕窗口验证,再决定要不要处理。


五、为什么 onMounted() 不会报错?

const width = ref(0)

onMounted(() => {
  width.value = window.innerWidth
})

执行顺序:

服务器 → 生成 HTML(width = 0)
浏览器显示(width = 0)
Hydration(0 = 0,一致!✅)
onMounted() → 修改 width = 1920(正常响应式更新)

第一轮服务器和浏览器都是 0,Hydration 成功。之后 onMounted 修改值是正常响应式更新,已经不是 Hydration,所以不会报错。

代价是页面可能先显示默认值再更新。如果这个值影响首屏结构(比如宽度决定显不显示侧边栏),用 <ClientOnly>fallback 更合适。


六、脑图

SSR Error 与 Hydration Mismatch 判断脑图

以后遇到相关错误,按三个问题依次判断:

  1. 服务器能否执行完成? 如果访问了不存在的 window,会在生成 HTML 前成为 SSR Error。
  2. 服务器是否已经返回 HTML? 没有 HTML 就不可能进入 Hydration。
  3. 客户端首次计算结果是否与服务器一致? 不一致才是 Hydration Mismatch。

七、代码实验

创建一个页面:

<script setup>
const width = ref(0)
if (import.meta.client) {
  width.value = window.innerWidth
}
</script>

<template>
  <div>{{ width }}</div>
</template>

npm run dev 打开页面,控制台会出现 Hydration mismatch 警告。改成 onMounted 版本后警告消失。

再确认一个细节:mismatch 警告主要在开发模式输出;生产环境可能不打印,但错误的修复逻辑仍然可能造成交互异常,所以要在开发阶段就修掉,而不是等线上出问题。

下一章会进入数据获取阶段,看看与服务器和浏览器"各执行一次"相关的另一个高频问题(为什么 useFetch 不是 fetch)。


八、容易踩坑

❌ window 导致的一定是 Hydration Error

不是。服务器直接执行 window,首先发生的是 SSR Error,页面根本打不开。

❌ onMounted 属于 SSR

不是。它发生在 Hydration → Mounted 之后,可以安全使用 windowdocumentlocalStorage

❌ 生产环境没有 mismatch 就不用管

不是。生产环境只是默认不打印警告,隐患还在。

❌ ClientOnly 是万能药

不是。它能让两边 HTML 一致,但也意味着首屏没有真实内容,可能影响 SEO 和首屏体验。能用 onMounted 解决的场景优先用 onMounted


一句话记忆

SSR Error 是服务器崩了;Hydration Error 是服务器和浏览器"对不上答案"。先用 onMounted,不行再用 <ClientOnly>