SSR 水合问题

为什么需要 SSR?

在正式回答水合之前,先理解一个问题:为什么要有 SSR?

传统的 CSR(客户端渲染) 流程是:浏览器下载一个空壳 HTML,然后 JS 下载执行后才渲染出内容。这导致两个核心问题:

  1. 首屏白屏时间长 — 用户盯着空白页面等 JS 下载执行
  2. SEO 不友好 — 搜索引擎爬虫抓到的也是空壳 HTML,没法索引内容

SSR(服务端渲染) 就是来解决这两个问题的——​在服务端就把完整的 HTML 生成好​,浏览器拿到的直接就是有内容的页面。

先看一张 SSR 和 CSR 的对比图:

SSR 的核心:一次请求,三阶段

一个 SSR 请求从用户浏览器点击到页面可交互,经历了三个阶段。

阶段一:服务端渲染

  1. 用户请求一个页面(比如 /products)
  2. Node.js 服务端(Vite/Webpack 帮我们做的)​同步获取数据​(调用 API 或查数据库)
  3. 把数据传入组件,​渲染成 HTML 字符串​(比如 React 的 renderToString())
  4. 把数据​序列化后嵌入 HTML​(常见做法是塞进 <script>window.__INITIAL_STATE__=...</script>)
  5. 返回完整 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 类触发原因

  1. 时间敏感数据 — new Date().toLocaleString()、Math.random(),服务端和客户端运行时不同结果
  2. 浏览器 API 依赖 — 读取 window.innerWidth 判断是移动端还是桌面端,服务端没有 window
  3. 数据在请求间隙发生变化 — 上面那个商品数量的例子
  4. 第三方脚本修改了 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 问题,但引入了服务端开销和水合复杂度。

posted @ 2026-07-07 17:28  秀秀不只会前端  阅读(35)  评论(0)    收藏  举报