Hydration
Hydration(水合) 是现代前端框架(如 Next.js)在服务端渲染(SSR)或静态生成(SSG)场景下的激活过程。
简单来说,它的作用是:将服务器生成的“静态”HTML 页面,变成浏览器中“动态”且可交互的 React 应用。
如果没有 Hydration,你看到的只是一个不能点击、不能输入的“死”页面。
🌊 什么是 Hydration?
你可以把服务器返回的 HTML 想象成一个**“干燥的骨架”**。
- 服务器:负责把肉(HTML 结构)和血(初始数据)填进去,快速展示给用户看(首屏快)。
- Hydration:负责把“神经”(事件监听器
onClick/onChange)和“灵魂”(组件状态useState/useEffect)注入进去,让页面“活”过来。
核心前提: Hydration 的过程不是删除旧的 DOM 重新渲染,而是复用服务器生成的 DOM 结构,仅仅在其上“绑定” JavaScript 逻辑。
🔄 Hydration 的完整流程
这是一个从用户敲回车到页面完全可用的过程:
-
服务端渲染 (SSR/SSG)
- 服务器接收到请求。
- 执行组件逻辑,获取数据,生成带有内容的纯 HTML 字符串。
- 将 HTML 发送给浏览器。
-
浏览器展示
- 浏览器收到 HTML,立即解析并显示内容(此时页面是静态的,点击按钮没反应)。
-
加载 JavaScript
- 浏览器同时下载页面所需的 JavaScript 包(React 运行时、组件代码等)。
-
客户端水合 (Hydration)
- JavaScript 执行,React 开始工作。
- 对比 (Reconciliation):React 会生成一份“虚拟 DOM”(Virtual DOM),并将其与服务器发来的“真实 DOM”进行比对。
- 绑定 (Binding):如果两者结构一致,React 会在现有的 DOM 节点上绑定事件监听器,并关联组件的状态。
- 完成:页面变成完全可交互的 SPA(单页应用)。
🚀 App Router 中的进化:Partial Hydration (部分水合)
在 Next.js 的 App Router 中,Hydration 变得更智能了,不再需要“全量”水合。
传统模式 (全量水合):整个页面必须等所有 JS 加载完才能交互。 App Router 模式 (部分水合/岛屿架构):
- 页面被拆分为 Server Components(服务端组件)和 Client Components(客户端组件)。
- Server Components:永远不需要 Hydration(因为它们只在服务端运行)。
- Client Components:只有这些“交互岛屿”需要 Hydration。
优势: 浏览器可以流式传输内容。先显示不需要交互的部分(如文章内容),后台悄悄加载交互组件(如评论框、轮播图)的 JS,等加载完再单独激活(Hydrate)那个小区域,极大提升了性能。
⚙️ 底层是如何实现的?
Hydration 的实现依赖于 React 对 DOM 的“接管”机制:
-
标记与序列化:
- 服务端渲染时,React 会给 DOM 节点添加特殊的
data-reactroot或data-reactid等属性,标记这是由 React 管理的节点。 - 对于 RSC(React Server Components),服务端还会发送一个
RSC Payload(一种特殊的 JSON 数据流),描述组件树的结构和数据。
- 服务端渲染时,React 会给 DOM 节点添加特殊的
-
客户端“注水”:
- 客户端 React 代码启动后,会寻找带有上述标记的 DOM 节点。
- 它会根据
RSC Payload或组件逻辑,在内存中重建虚拟 DOM 树。 - 关键步骤:React 会遍历真实 DOM,将事件监听器附加到对应的节点上,并建立真实 DOM 与虚拟 DOM 的映射关系。
-
协调 (Reconcile):
- React 确认客户端生成的虚拟 DOM 与服务端传来的 HTML 结构是否完全一致。
⚠️ 常见坑:Hydration Mismatch (水合不匹配)
这是开发中最常见的错误,浏览器会报 Text content did not match 或 Expected server HTML to contain a matching...。
原因: 服务器生成的 HTML 和 客户端第一次渲染的 Virtual DOM 结构不一致。
- 服务端:生成了
<div>加载中...</div>。 - 客户端:JS 加载后,立刻渲染了
<div>你好,用户</div>。 - 结果:React 想要复用 DOM,发现“加载中...”和“你好,用户”对不上,发生 Mismatch。
解决方案:
- 保持一致:确保服务端和客户端的初始渲染结果一致。
- 条件渲染延迟:涉及
window、document或localStorage的代码(只在浏览器存在),应该放在useEffect里,或者使用Suspensefallback。 - 使用
use client明确边界:在 App Router 中,明确划分服务端和客户端组件,避免逻辑混淆。
总结
| 阶段 | 状态 | 作用 |
|---|---|---|
| 服务端渲染 | 🏗️ 骨架搭建 | 快速展示内容,利于 SEO |
| Hydration | 💧 注入灵魂 | 绑定事件、恢复状态,让页面“活”过来 |
| 完成 | 🏃♂️ 完全交互 | 变成流畅的单页应用 |
Partial Hydration
Partial Hydration(部分水合)的工作原理可以形象地理解为**“按需激活”**。
在传统的全量水合(Full Hydration)中,浏览器必须等待页面所需的所有 JavaScript 代码都下载、解析完毕,才能一次性为整个页面的 DOM 绑定交互逻辑。这就像一座大楼,必须等所有装修材料(JS代码)都运到现场,工人才能开始工作,导致大楼在很长一段时间内都处于“不可用”状态。
而 Partial Hydration 则打破了这种“一刀切”的模式,它的核心流程如下:
1. 核心机制:选择性激活
Partial Hydration 的本质是将页面拆分为“静态部分”和“动态部分”。
-
静态内容(Static Content):指那些不需要交互、内容相对固定的部分,例如博客文章的正文、新闻标题、商品描述等。
- 处理方式:这些部分仅由服务器渲染成纯 HTML 发送给浏览器。浏览器显示它们,但不需要为其加载对应的 JavaScript 代码,也不需要进行水合。
- 结果:它们就是纯粹的静态文档,加载极快,不消耗客户端计算资源。
-
动态组件(Dynamic Components):指那些需要用户交互的部分,例如评论区、轮播图、购物车、表单按钮等。
- 处理方式:这些组件被标记为“待水合”。浏览器会单独加载这些组件所需的 JavaScript 代码,并仅在这些特定的 DOM 节点上执行水合过程,绑定事件监听器和状态。
- 结果:这些“交互岛屿”被“激活”,变得可点击、可操作。
2. 工作流程:静态先行,动态跟进
这是一个从服务器到浏览器的协作过程:
- 服务器拆分:服务器接收到请求后,会分析页面结构。它将页面拆解,把静态内容直接生成 HTML,而将动态组件(Client Components)标记为占位符,并附上一个“加载指令”。
- 流式传输:服务器立即将包含静态 HTML 和占位符的响应流式传输给浏览器。用户能立刻看到大部分内容(如文章),此时页面主体是静态的。
- 后台加载 JS:浏览器在展示静态内容的同时,在后台异步、优先加载动态组件所需的 JavaScript 包。
- 局部水合:一旦某个动态组件的 JS 代码加载完成,React 会立即找到对应的 DOM 占位符,执行水合,将其“激活”。这个过程是独立的,不影响页面其他部分。
3. 在 Next.js App Router 中的实现
在 Next.js App Router 中,Partial Hydration 是通过 React Server Components (RSC) 架构天然实现的。
- Server Components:默认就是“静态”的。它们只在服务器运行,生成 HTML 后使命终结,永远不会在客户端进行水合。这极大地减少了需要传输和执行的 JavaScript 体积。
- Client Components:只有这些组件需要水合。通过
use client指令声明的组件,框架会知道需要为其生成并传输相应的客户端代码,并在合适时机执行局部水合。
4. 与传统全量水合的对比
| 特性 | 全量水合 (Full Hydration) | 部分水合 (Partial Hydration) |
|---|---|---|
| JS 下载 | 必须下载整个应用的 JS 包 | 仅下载动态组件的 JS(按需) |
| 激活时机 | 所有 JS 加载完后,一次性激活整个页面 | 静态内容立即可见,动态组件加载完即刻激活 |
| 资源消耗 | 高(主线程被长时间阻塞) | 低(分块处理,减少主线程压力) |
| 用户体验 | 首屏可能出现白屏或长时间不可交互 | 首屏极速展示,交互功能逐步可用 |
总结来说,Partial Hydration 通过只对页面中必要的交互部分进行“水合”,最大限度地减少了客户端的 JavaScript 负载和计算任务,从而实现了“静态内容即时可见,动态组件按需激活”的极致性能体验。