SSR 水合问题
为什么需要 SSR?
在正式回答水合之前,先理解一个问题:为什么要有 SSR?
传统的 CSR(客户端渲染) 流程是:浏览器下载一个空壳 HTML,然后 JS 下载执行后才渲染出内容。这导致两个核心问题:
- 首屏白屏时间长 — 用户盯着空白页面等 JS 下载执行
- SEO 不友好 — 搜索引擎爬虫抓到的也是空壳 HTML,没法索引内容
SSR(服务端渲染) 就是来解决这两个问题的——在服务端就把完整的 HTML 生成好,浏览器拿到的直接就是有内容的页面。
先看一张 SSR 和 CSR 的对比图:

SSR 的核心:一次请求,三阶段
一个 SSR 请求从用户浏览器点击到页面可交互,经历了三个阶段。
阶段一:服务端渲染
- 用户请求一个页面(比如
/products) - Node.js 服务端(Vite/Webpack 帮我们做的)同步获取数据(调用 API 或查数据库)
- 把数据传入组件,渲染成 HTML 字符串(比如 React 的
renderToString()) - 把数据序列化后嵌入 HTML(常见做法是塞进
<script>window.__INITIAL_STATE__=...</script>) - 返回完整 HTML
阶段二:浏览器展示
浏览器收到 HTML 后,直接开始渲染。用户立刻就能看到页面内容,不需要等 JS 下载完。这就是 SSR 最大的优势——首次内容绘制(FCP)极快。
阶段三:水合(Hydration)
浏览器同时下载 JS bundle,下载完成后开始 「水合」:
- 重建组件树(重新跑一遍组件代码)
- 把服务端生成的 DOM 结构跟组件树做 「对账」(reconciliation)
- 对账通过后,绑定事件监听器,页面才变得可交互
水合问题详解
什么是"水合"?这个名字取自化学里的"水合物"概念——把干的(服务端 HTML)加水(JS 事件绑定),让它重新活过来。
水合的核心流程:
| 步骤 | 做了什么 |
|---|---|
| 服务端 | renderToString(Component)→ 产出 HTML 字符串 |
| 浏览器收到 | 立即展示 HTML(用户看到内容了) |
| 浏览器下载 JS | 解析执行组件代码,重建虚拟 DOM |
| 水合开始 | 虚拟 DOM 和已有 DOM 做对账(reconciliation) |
| 对账通过 | 保留 DOM,只绑定事件监听器 |
| 对账失败 | 丢弃服务端 DOM,重新渲染 → 水合失败 |
水合失败的典型案例

看图。假设一个电商页面上显示"商品数量:10",这是服务端渲染时查数据库得到的。但 JS bundle 下载 + 解析完成时,后台已经新增了 2 件商品,客户端组件重新跑代码拿到的是 12。于是:
- 服务端 DOM 文本节点:
商品数量: 10 - 客户端虚拟 DOM 文本:
商品数量: 12
React 对账发现 10 !== 12 → 它认为服务端的 DOM 是错的 → 用客户端的覆盖它 → 闪屏。
更糟的情况:如果组件的结构都不一样了(比如条件渲染),框架会直接卸载整个子树重新挂载,SSR 的首屏加速效果全部白费。
最常见的 4 类触发原因
- 时间敏感数据 —
new Date().toLocaleString()、Math.random(),服务端和客户端运行时不同结果 - 浏览器 API 依赖 — 读取
window.innerWidth判断是移动端还是桌面端,服务端没有window - 数据在请求间隙发生变化 — 上面那个商品数量的例子
- 第三方脚本修改了 DOM — 广告脚本、分析脚本在 JS bundle 加载前已经改了 HTML
三种主流解决方案
方案一:消除不同源
这是最彻底的解法——客户端水合时不要重新请求数据,直接用服务端序列化过来的那一份:
<!-- 服务端在 HTML 末尾嵌入 -->
<script>
window.__INITIAL_STATE__ = {
products: 10, // 跟渲染 HTML 时用同一个值
user: { name: '用户' }
}
</script>
客户端水合时读取 window.__INITIAL_STATE__,不用重新 fetch,保证两端数据一致。
方案二:抑制水合警告(框架常用做法)
对于注定不同的动态内容(比如"当前在线人数"这种实时数据),主动告诉框架"别管了":
- React:
suppressHydrationWarning={true} - Vue/Nuxt:
<ClientOnly>包裹,服务端不渲染这部分 - Next.js:
'use client'+ 动态导入禁用 SSR
方案三:流式 SSR + 选择性水合
React 18 引入的架构级改进:
- Suspense 包裹每个区域独立 SSR,HTML 流式返回,不必等全部数据
- 选择性水合——用户点击了某个按钮,React 优先水合那个交互区域,而不是从头到尾水合全部
- Server Components(RSC)——部分组件永远只在服务端运行,不发送 JS 到客户端,从源头消除水合问题
总结
CSR(客户端渲染)
请求 -> 返回空壳 -> 下载 JS -> 渲染 -> 可交互 ❌ 白屏期长
SSR(服务端渲染)
请求 -> 服务端渲染 HTML -> 返回完整 HTML ->
立即可见 + 后台下载 JS -> 水合 -> 可交互 ✅ 首屏快
水合问题
服务端 HTML 和客户端虚拟 DOM 不一致 ->
闪屏 / 重渲染 / SSR 白费 ⚠️ 需要处理
SSR 不是银弹——它解决了首屏速度和 SEO 问题,但引入了服务端开销和水合复杂度。

浙公网安备 33010602011771号