为什么 SSR 一定会有 hydration mismatch?

之前说到过 Hydration Mismatch 原理,今天来说说 为什么从设计上,它就无法完全避免

一、先把问题还原到最本质

SSR 做了两件事:

  1. 服务端生成 HTML
  2. 客户端接管(Hydration)

Hydration 的本质是:

在不重建 DOM 的情况下,让 JS 接管已有 HTML (原理详解)

这里有一个隐含前提

客户端"重新执行一遍渲染逻辑",并且结果必须和服务端完全一致 (原理详解)


二、关键矛盾:同一份代码,在两个环境执行

这是问题的根源。

SSR 架构本质是:

text 复制代码
同一套组件逻辑
在两个环境执行:
- Node(服务端)
- Browser(客户端)

问题在于:

这两个环境永远不可能完全一致


三、用一个最简单的例子说明

js 复制代码
<span>{new Date().getMilliseconds()}</span>

服务端输出:

html 复制代码
<span>123</span>

客户端执行:

html 复制代码
<span>167</span>

结果:

不一致 → mismatch (Vite-plugin-ssr)

所以推导出一个结论:只要渲染依赖"运行时",就必然存在不一致的可能


四、为什么"不一致"是必然的

从几个维度拆开看:


1. 时间是不一致的

js 复制代码
Date.now()
new Date()
  • 服务端时间 ≠ 客户端时间
  • 网络延迟会放大差异

结论:

只要用时间,就有 mismatch 风险 (Vite-plugin-ssr)


2. 环境是不一致的

服务端没有:

js 复制代码
window
document
localStorage

客户端有,典型代码:

js 复制代码
if (typeof window !== 'undefined') {
  // client logic
}

结果:

  • 服务端渲染 A
  • 客户端渲染 B

直接 mismatch


3. 状态是不一致的

例如:

js 复制代码
if (localStorage.getItem('token')) {
  return <Home />
} else {
  return <Login />
}

服务端:

  • 没有 localStorage → Login

客户端:

  • 有 token → Home

结果:

DOM 完全不同


4. 随机性是不一致的

js 复制代码
Math.random()
uuid()

服务端生成一套

客户端再生成一套

结论:

不可能一致


5. 执行顺序是不一致的

例如:

  • async 数据
  • 并发渲染
  • 不稳定 ID 生成

再次重复一开始提到的前提:SSR 本质是"重复计算",而重复计算无法保证一致性


五、为什么框架"必须要求一致"?

问题来了:

为什么一定要一致?

因为 Hydration 的优化前提是:

text 复制代码
复用已有 DOM
而不是重新创建

如果不一致:

框架只能:

  1. 丢弃 DOM
  2. 重新渲染

这会导致:


一个更本质的矛盾

SSR 想要:

提前生成 HTML(提高首屏)

但 Hydration 需要:

再执行一遍渲染逻辑

这本身就是:

一次"重复执行系统"

而所有重复执行系统都有一个问题:

一致性无法保证


六、工程上怎么"缓解",但不是"解决"(mismatch 只能减少,无法彻底消灭)

常见手段:

1. 保证初始数据一致

js 复制代码
// server 计算
const data = fetch()

// 注入 HTML
window.__INITIAL_DATA__ = data

2. 延迟客户端逻辑

js 复制代码
useEffect(() => {
  // client only
}, [])

3. 避免不确定性

不要在 render 中用:

  • Date
  • Math.random
  • 浏览器 API

4. 架构层优化

例如:

  • Islands Architecture
  • Partial Hydration

本质是:

减少需要 hydration 的范围


七、讨论一下 Islands:为什么这个架构本质是在"逃避 hydration"

先看传统 SSR 流程:

text 复制代码
Server Render HTML
        ↓
Browser 接收 HTML
        ↓
整页 Hydration
        ↓
页面可交互

问题在于:

页面里很多内容其实根本不需要交互。

例如一个电商详情页:

  • 商品标题
  • 商品描述
  • banner
  • 评论文本
  • footer

这些内容:

  • 需要 SEO
  • 需要首屏展示
  • 但不需要 JS 接管

然而传统 SSR 会做什么?

即使这些内容完全静态,也要执行 hydration。

也就是说:

text 复制代码
静态内容 + 动态内容
全部进入 hydration

这就带来两个问题:

1. 不必要的 JS 执行

例如页面结构:

html 复制代码
<div>
  Header
  ProductInfo
  Comments
  BuyButton
  Footer
</div>

真正需要交互的可能只有:

text 复制代码
BuyButton

但传统 hydration:

text 复制代码
Header hydrate
ProductInfo hydrate
Comments hydrate
BuyButton hydrate
Footer hydrate

本质上:

为了一个按钮,整页都要重复执行一遍。

这很浪费。


2. mismatch 风险被放大

前面说过:

hydration 本质是重复计算

那只要重复计算范围越大:

  • 时间差异
  • 环境差异
  • 状态差异

都会扩大。

整页 hydration 意味着:

整页都有 mismatch 风险。


Islands 的思路:不要整页 hydration

Islands 架构把页面拆成:

  • Static HTML
  • Interactive Islands

例如:

html 复制代码
<Header />
<ProductInfo />
<BuyButton client:load />
<Footer />

只有:

text 复制代码
BuyButton

需要客户端接管。

其余部分:

  • 只保留 HTML
  • 永不 hydration

流程变成:

text 复制代码
Server Render HTML
        ↓
静态部分直接展示
        ↓
仅局部组件 hydration

本质变化

传统 SSR:

HTML 先渲染,JS 再接管整页

Islands:

HTML 默认静态,只有少数区域需要 JS

所以它的核心思想不是:

"怎么更高效地 hydration"

而是:

尽量少 hydration,甚至不 hydration

这就是为什么说:

Islands 本质是在逃避 hydration。

不是因为 hydration 做得不好,而是因为:

hydration 天然昂贵且天然存在一致性问题。

既然如此,最好的办法不是优化它,而是减少它。


这样带来的收益

  1. JS 体积更小,只加载交互组件代码,不是整页 bundle。
  2. hydration 更快,因为只 hydration 局部。
  3. mismatch 风险更低,范围缩小:整页风险 → 局部风险
  4. 更接近"默认静态",因为现代 Web 一个趋势是:Static by default, interactive when needed,也就是:
  • 默认静态
  • 局部增强

这和传统 SPA 思路完全相反。


一个更高的理解

SSR 第一阶段在解决:

首屏白屏问题

Hydration 第二阶段在解决:

HTML 如何变成交互页面

而 Islands 的想法是:

并不是所有 HTML 都需要变成交互页面。

所以它直接重新定义问题。

这就是架构升级。

不是:How to hydrate better

而是:How to hydrate less


八、最终结论

Hydration mismatch 不是一个"bug",而是 SSR 架构的副作用。

因为 SSR 本质是让同一段渲染逻辑在两个不同环境执行,而只要存在时间、环境、状态或随机性的差异,就无法保证输出完全一致。

框架要求一致,是为了复用 DOM 提升性能;而不一致,就只能回退到重新渲染。

因此,hydration mismatch 并不是"可以彻底避免的问题",而是一个需要被工程化控制的问题。

Traditional SSR 的问题不是不能工作,而是默认假设"整页都需要交互"。

但现实中,大部分页面内容本质是静态的。

Islands 架构通过把页面拆分为静态区域和交互区域,只对少量组件执行 hydration。

因此它并不是在优化 hydration,而是在架构层面减少 hydration 的发生范围。

某种意义上,Islands 并不是解决 hydration 的问题,而是在逃避 hydration 本身。


相关推荐
lichenyang45311 小时前
Docker 学习笔记(一):为什么需要镜像、容器和仓库?
前端
kyriewen11 小时前
别再对着 TypeScript 报错发呆了:我把 10 个最常见的红色波浪线翻译成了人话
前端·javascript·typescript
IT_陈寒11 小时前
SpringBoot自动配置的坑,我的API突然就404了
前端·人工智能·后端
奇奇怪怪的12 小时前
Embedding 模型 10+ 横向评测
前端
陈广亮12 小时前
Monorepo 从 0 到 1 实操指南 2026 版:pnpm catalogs + Turborepo 2.x + changesets 全链路
前端
子兮曰12 小时前
OpenMontage 深度解剖:你的 AI 编程助手,其实是个视频工作室
前端·后端·ai编程
敲代码的鱼12 小时前
PDF 预览与签名批注写回 支持安卓 iOS 鸿蒙 UTS插件
android·前端·ios
子兮曰12 小时前
前端工具链的「Rust 化」:一场没有赢家的军备竞赛?
前端·后端·rust
Hyyy13 小时前
Function Calling / Tool Use的原理和实现模式
前端·llm·ai编程