惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
L
LangChain Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
Microsoft Security Blog
Microsoft Security Blog
J
Java Code Geeks
博客园 - 【当耐特】
量子位
有赞技术团队
有赞技术团队
Jina AI
Jina AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
C
Check Point Blog
B
Blog RSS Feed
M
MIT News - Artificial intelligence
H
Help Net Security
雷峰网
雷峰网
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 聂微东
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
A
About on SuperTechFans
腾讯CDC

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解
为什么 SSR 一定会有 hydration mismatch?
幼儿园技术家 · 2026-04-29 · via 博客园_首页

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

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

SSR 做了两件事:

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

Hydration 的本质是:

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

这里有一个隐含前提

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


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

这是问题的根源。

SSR 架构本质是:

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

问题在于:

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


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

<span>{new Date().getMilliseconds()}</span>

服务端输出:

<span>123</span>

客户端执行:

<span>167</span>

结果:

不一致 → mismatch (Vite-plugin-ssr)
所以推导出一个结论:只要渲染依赖“运行时”,就必然存在不一致的可能


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

从几个维度拆开看:


1. 时间是不一致的

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

结论:

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


2. 环境是不一致的

服务端没有:

window
document
localStorage

客户端有,典型代码:

if (typeof window !== 'undefined') {
  // client logic
}

结果:

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

直接 mismatch


3. 状态是不一致的

例如:

if (localStorage.getItem('token')) {
  return <Home />
} else {
  return <Login />
}

服务端:

  • 没有 localStorage → Login

客户端:

  • 有 token → Home

结果:

DOM 完全不同


4. 随机性是不一致的

Math.random()
uuid()

服务端生成一套
客户端再生成一套

结论:

不可能一致


5. 执行顺序是不一致的

例如:

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

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


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

问题来了:

为什么一定要一致?

因为 Hydration 的优化前提是:

复用已有 DOM
而不是重新创建

如果不一致:

框架只能:

  1. 丢弃 DOM
  2. 重新渲染

这会导致:


一个更本质的矛盾

SSR 想要:

提前生成 HTML(提高首屏)

但 Hydration 需要:

再执行一遍渲染逻辑

这本身就是:

一次“重复执行系统”

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

一致性无法保证


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

常见手段:

1. 保证初始数据一致

// server 计算
const data = fetch()

// 注入 HTML
window.__INITIAL_DATA__ = data

2. 延迟客户端逻辑

useEffect(() => {
  // client only
}, [])

3. 避免不确定性

不要在 render 中用:

  • Date
  • Math.random
  • 浏览器 API

4. 架构层优化

例如:

  • Islands Architecture
  • Partial Hydration

本质是:

减少需要 hydration 的范围


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

先看传统 SSR 流程:

Server Render HTML
        ↓
Browser 接收 HTML
        ↓
整页 Hydration
        ↓
页面可交互

问题在于:

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

例如一个电商详情页:

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

这些内容:

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

然而传统 SSR 会做什么?

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

也就是说:

静态内容 + 动态内容
全部进入 hydration

这就带来两个问题:

1. 不必要的 JS 执行

例如页面结构:

<div>
  Header
  ProductInfo
  Comments
  BuyButton
  Footer
</div>

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

BuyButton

但传统 hydration:

Header hydrate
ProductInfo hydrate
Comments hydrate
BuyButton hydrate
Footer hydrate

本质上:

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

这很浪费。


2. mismatch 风险被放大

前面说过:

hydration 本质是重复计算

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

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

都会扩大。

整页 hydration 意味着:

整页都有 mismatch 风险。


Islands 的思路:不要整页 hydration

Islands 架构把页面拆成:

  • Static HTML
  • Interactive Islands

例如:

<Header />
<ProductInfo />
<BuyButton client:load />
<Footer />

只有:

BuyButton

需要客户端接管。

其余部分:

  • 只保留 HTML
  • 永不 hydration

流程变成:

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 本身。