学习目标
理解什么是 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 更合适。
六、脑图
以后遇到相关错误,按三个问题依次判断:
- 服务器能否执行完成? 如果访问了不存在的
window,会在生成 HTML 前成为 SSR Error。 - 服务器是否已经返回 HTML? 没有 HTML 就不可能进入 Hydration。
- 客户端首次计算结果是否与服务器一致? 不一致才是 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 之后,可以安全使用 window、document、localStorage。
❌ 生产环境没有 mismatch 就不用管
不是。生产环境只是默认不打印警告,隐患还在。
❌ ClientOnly 是万能药
不是。它能让两边 HTML 一致,但也意味着首屏没有真实内容,可能影响 SEO 和首屏体验。能用 onMounted 解决的场景优先用 onMounted。
一句话记忆
SSR Error 是服务器崩了;Hydration Error 是服务器和浏览器"对不上答案"。先用
onMounted,不行再用<ClientOnly>。