hydration 是让服务端生成的静态 html“活过来”,复用 dom 并绑定事件、状态与生命周期;必须严格对齐服务端 html 与客户端组件树,否则触发 mismatch 错误;数据通过内联 script 注入,react 由 hydrateroot 执行接管。

Hydration 不是重画页面,而是让服务端生成的静态 HTML“活过来”——在不重建 DOM 的前提下,把事件监听、状态管理、组件生命周期这些“交互能力”补上去。
它怎么接管?复用 DOM + 绑定逻辑
服务端吐出的 HTML 是纯结构(比如一个带 class 的 <button>提交</button>),没有点击行为、没有状态、也不响应输入。Hydration 就是在浏览器加载完 JS 后,用客户端框架(React/Vue)的虚拟 DOM 去“对齐”这个 HTML:
- 逐节点比对:检查服务端 HTML 的标签、属性、文本内容是否和客户端组件预期的一致
- 跳过渲染:只要对得上,就直接复用现有 DOM 节点,不删不建
- 只加能力:给按钮绑
onClick,恢复计数器值,挂载 useEffect 或 mounted 钩子
为什么必须“对得上”?Hydration Mismatch 就是卡在这儿
一旦服务端 HTML 和客户端组件树不一致,就会触发 Hydration mismatch 错误,常见情况有:
- 服务端渲染时用了
Date.now()或Math.random(),导致两端时间/随机值不同 - 客户端有依赖
window或navigator的逻辑,服务端没这些 API,渲染结果自然不同 - 样式库(如 emotion)或 SSR 配置不当,导致 class 名在两端生成规则不一致
- 条件渲染分支在服务端和客户端判断结果相反(比如未登录态 vs 登录态)
数据怎么从服务端传到客户端?靠内联脚本
服务端不会直接把数据塞进 JSX,而是通过 HTML 中的 <script></script> 标签安全注入:
- Next.js 用
self.__next_f = [...],Nuxt 用window.__NUXT__ = {...} - 客户端 JS 启动时,先读取这个全局变量,再作为 props 传给根组件
- 这样既避免了额外请求,又保证 hydration 一开始就有正确初始状态
实际接管动作由谁执行?框架底层函数
以 React 为例,入口是 hydrateRoot(container, element),它内部会:
- 标记容器为 hydration 模式(不是 mount)
- 遍历 DOM 树,尝试“认领”每个可水合节点(
tryToClaimNextHydratableInstance) - 对文本节点、元素节点、Suspense 边界分别处理,确保类型和顺序严格匹配
- 最后挂载事件系统,使整个页面真正可交互











