在使用 SSR(Server Side Rendering) 框架(如 Nuxt / Next)时,开发者几乎都会遇到一个问题:
Hydration mismatch
轻则控制台 warning,重则页面重新渲染、闪屏、性能下降,甚至功能异常。那么 Hydration 是什么、为什么会 mismatch,在工程中如何系统性避免捏?
一、什么是 Hydration
在 SSR 项目中,页面渲染实际上分为两个阶段:
- 服务器渲染(SSR):生成 HTML
- 客户端接管(Hydration):绑定事件、恢复状态
Hydration 的定义可以概括为:
在不重新创建 DOM 的前提下,让客户端 JS 接管服务器生成的 HTML,使得一块干燥的海绵有了水分的注入变得能有 "交互性"(捏一捏挤出水)
1.1 为什么需要 Hydration
如果没有 Hydration,客户端接管一个有 "间谍" 的 HTML,只能:
- 删除已有 DOM
- 重新执行一次 CSR 渲染
这会导致:
- 首屏闪烁
- 性能浪费
- 用户体验下降
因此现代 SSR 框架都会尝试 复用已有 DOM,只在必要时补充缺失的属性或绑定事件,这就是 Hydration 的意义。
二、Hydration 过程中做了什么
以 Vue / React 为例,Hydration 主要做三件事:
- 遍历已有 DOM
- 生成虚拟 DOM
- 对比并绑定事件
核心前提只有一个:
客户端生成的虚拟 DOM 必须与服务器生成的 HTML 结构完全一致
否则就会出现 mismatch。
三、什么是 Hydration Mismatch
Hydration mismatch 指的是:
客户端首次渲染得到的虚拟 DOM,与服务器返回的 HTML 不一致
此时框架会:
- 给出 warning(开发环境)
- 丢弃已有 DOM
- 重新执行一次完整的客户端渲染
3.1 常见警告示例(Vue)
text
Hydration completed but contains mismatches.
或者:
text
Text content does not match server-rendered HTML.
四、为什么会产生 Mismatch(核心原因)
本质原因(只有一个)
SSR 与 CSR 执行环境不同,但代码写成了"依赖运行时环境"的形式
4.1 使用了浏览器专属对象
js
const width = window.innerWidth
- 服务端:
window不存在 - 客户端:存在
导致渲染结果不一致。
4.2 使用了不稳定的值
1️⃣ 时间相关
vue
<template>
<div>{{ Date.now() }}</div>
</template>
- SSR:构建时间
- CSR:当前时间
结果必然不一致。
2️⃣ 随机数
js
Math.random()
3️⃣ 非确定性排序
js
list.sort(() => Math.random() - 0.5)
4.3 条件渲染依赖客户端状态
vue
<div v-if="isMobile">Mobile</div>
js
const isMobile = window.innerWidth < 768
SSR 无法得知客户端宽度。
4.4 服务端与客户端数据不一致
js
// 服务端
const data = await fetch('/api/data')
// 客户端
const data = await fetch('/api/data')
如果数据在两次请求之间发生变化,就会 mismatch。
4.5 HTML 结构不合法
html
<p>
<div>content</div>
</p>
浏览器会自动修正 DOM 结构,导致:
- SSR 输出 ≠ 浏览器实际 DOM
五、Hydration Mismatch 的"隐性后果"
即使页面"看起来正常",仍然可能存在问题:
- 页面被强制重新渲染
- 首屏性能指标下降(LCP / FCP)
- 事件绑定延迟
- 某些节点丢失状态
这也是为什么 不应该忽视 warning。
六、如何系统性避免 Hydration Mismatch
6.1 核心原则(非常重要)
首屏渲染必须是"纯函数"
- 相同输入
- 相同输出
- 不依赖运行环境
6.2 延迟到客户端执行(onMounted)
js
const width = ref(0)
onMounted(() => {
width.value = window.innerWidth
})
6.3 使用客户端专用组件
Nuxt 示例
vue
<ClientOnly>
<Chart />
</ClientOnly>
6.4 使用 process.client / import.meta.client
js
if (import.meta.client) {
// 只在客户端执行
}
6.5 保证数据只在一侧生成
服务端生成 → 客户端复用
js
useAsyncData('list', fetchList)
6.6 对不可避免的差异进行兜底
vue
<div v-if="mounted">
{{ clientOnlyValue }}
</div>
6.7 骨架屏、占位符结合判断也是不错的想法哟~
七、排查 Hydration Mismatch 的思路
- 关注首个 warning
- 锁定报错节点
- 排查是否使用了不稳定值
- 检查条件渲染
- 确认数据是否重复请求
- 查看 HTML 结构是否合法
八、错误示例与修复
错误示例
vue
<template>
<div>
{{ new Date().toLocaleString() }}
</div>
</template>
修复方案
vue
<template>
<div>
{{ time }}
</div>
</template>
<script setup>
const time = ref('')
onMounted(() => {
time.value = new Date().toLocaleString()
})
</script>
九、框架层面的设计取舍
需要明确一点:
Hydration mismatch 并不是框架 bug,而是开发者违反了 SSR 的约束条件
SSR 框架已经尽可能"宽容",但它无法猜测开发者的真实意图,so you 要去迎合它。
十、SO
Hydration mismatch 的核心结论只有三点:
- SSR 与 CSR 必须输出一致的 HTML
- 首屏渲染不能依赖运行时环境
- 不确定性逻辑必须延迟到客户端
如果你把 SSR 页面当作:
一个"可复现的纯函数渲染结果"
那么 Hydration mismatch 将大幅减少。
Finally
在实际项目中,我的建议是:
- 能 SSG 的页面,尽量 SSG
- 能 CSR 的交互,尽量 CSR
- SSR 页面只承载"稳定首屏内容"