什么是 Hydration?为什么会出现水合不一致?
简化版
Hydration 是客户端接管服务端已渲染 HTML 的过程:浏览器先展示服务端 HTML,然后下载 JS,框架绑定事件并恢复组件状态。水合不一致通常是因为服务端和客户端首次渲染结果不同,比如使用随机数、时间、浏览器 API、环境差异或异步数据不一致。
详细版
SSR 页面初始 HTML 来自服务端,但交互能力需要客户端 JS 接管。
Hydration 做的事:
- 复用已有 DOM。
- 建立组件实例。
- 绑定事件监听。
- 恢复状态。
- 检查客户端渲染结果是否和服务端 HTML 匹配。
水合不一致原因:
Date.now()、Math.random()。- 服务端没有 window/document。
- 客户端和服务端数据不同。
- 用户登录态只在客户端可知。
- 条件渲染依赖设备宽度。
- HTML 结构不合法被浏览器自动修正。
解决方式是保证首屏渲染确定性,把客户端专属逻辑延后到 mounted/effect。
完整版教学
一、为什么需要 Hydration
SSR 返回的 HTML 能让用户快速看到页面,但 HTML 本身没有完整前端应用状态。按钮点击、组件状态、路由跳转等能力需要客户端 JS 接管。
Hydration 就是把静态 HTML 变成可交互应用的过程。
它不是重新生成一遍 DOM,而是尽量复用服务端已经生成的 DOM,然后绑定事件和状态。
二、水合不一致是什么
框架会在客户端重新计算首屏应该长什么样。如果计算结果和服务端 HTML 不一致,就会出现 hydration mismatch。
轻则控制台警告,重则客户端丢弃服务端 DOM 重新渲染,导致闪烁、性能下降甚至页面错误。
三、常见原因和处理
随机数、当前时间、浏览器宽度、localStorage、用户时区等都会导致服务端和客户端结果不同。
处理原则:
- 首屏渲染使用确定数据。
- 浏览器 API 放到客户端生命周期中。
- 依赖用户环境的内容先占位,客户端接管后再渲染。
- 服务端和客户端共用同一份初始数据。
四、面试追问与工程落地
面试官可能问:“为什么服务端不能直接读取 localStorage?”
localStorage 是浏览器 API,服务端没有浏览器环境。服务端只能读取请求中的 Cookie、Header、URL 等信息。
工程中 SSR 代码要区分通用逻辑和客户端专属逻辑,避免在服务端渲染阶段访问 window、document、navigator。
五、从 HTML 到可交互页面的时间线
0ms 收到 HTML → 80ms 首屏可见 → 300ms JS 下载完成
→ 360ms 组件树重建/核对 → 420ms 事件可用
上例中内容在 80ms 可见,但按钮到 420ms 才真正可交互;这段间隔常被称为 hydration gap。若 JS 很大或主线程繁忙,SSR 的“看得见”不等于“点得动”。
| 阶段 | 浏览器/框架工作 | 可能瓶颈 |
|---|---|---|
| HTML 解析 | 建 DOM、发现资源 | 文档体积、阻塞资源 |
| JS 加载 | 下载与解析 bundle | 体积、网络、缓存 |
| 首次客户端渲染 | 用相同输入计算树 | 不确定数据 |
| 水合提交 | 复用 DOM、绑定事件 | CPU、组件数量 |
水合的契约是“服务端输出与客户端第一次计算一致”;客户端第二次 effect 更新可以不同,但第一次不能偷偷依赖浏览器环境。
六、定位 mismatch 的方法和边界
先保存服务器原始 HTML,再在禁用扩展的干净浏览器中捕获水合前 DOM,比较文本、属性和节点层级。浏览器会自动修正非法 HTML,例如把不允许嵌套的元素移位,所以源字符串看似一致,实际 DOM 也可能不同。
将 Date.now()、随机 id、时区格式化、A/B 分组、媒体查询和 localStorage 逐项替换成服务器注入的确定值。比如服务器生成随机 id a12,客户端必须复用 a12,不能再调用一次 random 得到 b91。
suppressHydrationWarning 只适合明确、局部、不可避免的文本差异,不是修复手段;滥用会隐藏真实结构错误。客户端-only 可用于地图、编辑器等确实无法 SSR 的组件,但会失去该模块的首屏 HTML 与 SEO 收益。
// 首屏先使用服务器传入值,挂载后再读取浏览器偏好
const initialTheme = serverTheme
// effect/mounted 中读取 localStorage 并更新
七、常见误区与追问
- 误区:水合不一致只是无害的控制台警告。 框架可能重建 DOM、丢失状态或产生事件和视觉异常。
- 误区:用
typeof window条件渲染就一定正确。 服务端 false、客户端首次 true 仍会生成不同树。 - 误区:所有问题都用 client-only 包裹最省事。 会放弃 SSR 内容并推迟模块可见和可交互时间。
- 追问:为什么非法 HTML 会导致 mismatch? 浏览器解析器会自动修正节点结构,与框架预期虚拟树不同。
- 追问:时间和时区怎样稳定? 服务端传入固定时间值和明确时区,客户端首次渲染复用,挂载后再本地化。
- 追问:水合为什么消耗 CPU? 客户端仍要加载代码、重建组件状态并核对/绑定已有 DOM。
- 追问:如何缩短 hydration gap? 减少客户端 JS、拆分交互边界、延迟非关键脚本并避免主线程长任务。
八、加强记忆
- 定义:水合让已有 SSR DOM 获得组件状态和事件能力。
- 核心合同:同样初始输入必须产生同样首屏树。
- 高危来源:时间、随机数、时区、浏览器 API、非法 HTML 和不同数据。
- 定位方法:比较原始 HTML、水合前 DOM 与客户端首次计算。
- 正确修复:注入确定值,浏览器专属变化放挂载后。
- 性能目标:不仅看内容可见,还要测 JS 成本和交互可用时间。