深入理解 React 水合(Hydration):从静态 HTML 到可交互页面

在 Next.js 开发中,你一定会遇到一个词:水合(Hydration) 。

很多同学第一次听到这个词会一脸懵:水合?是化学反应吗?跟渲染有什么关系?

其实说白了,水合就是让服务端渲染出来的"静态 HTML"活过来的过程。这篇文章用最直白的语言,带你彻底搞懂水合的原理、流程、坑点和最佳实践。


一、水合到底是什么?

想象一下这个场景:

你在餐厅点了一份水饺,服务员端上来一盘已经煮熟的饺子 (服务端渲染好的 HTML)。

饺子看起来完整、能吃,但如果你要蘸醋、加辣椒油,得自己动手。

水合就是你自己调蘸料、动筷子的过程------让静态的饺子变成一顿可以"交互"的饭。

对应到 React:

  • 服务端渲染(SSR) :服务器把组件跑一遍,生成完整的 HTML 字符串发给浏览器。此时页面有内容但无交互,就像煮熟的饺子,能吃但没蘸料。
  • 水合(Hydration) :浏览器下载并执行 JS 后,React 在客户端接管这个静态 HTML,给每个元素绑定事件监听器、恢复组件状态。于是,按钮能点了,输入框能打字了,页面"活"了。

一句话概括:水合是 React 将事件监听器和内部状态附加到服务端渲染的静态 HTML 上的过程。


二、为什么需要水合?

你可能会问:SSR 已经返回了完整的 HTML,为什么还要再执行一遍 JS?

因为 HTML 只是 UI 的"快照",没有交互能力。

服务端渲染的 HTML 长这样:

css 复制代码
<button>点击我</button>

但这个按钮在浏览器里是"死"的,点击没反应。因为没有任何 JS 给它绑定 onClick 事件。

要让按钮能响应点击,必须执行客户端 JS,React 内部会创建一个虚拟 DOM 树,然后与服务器生成的真实 DOM 对比,最后把事件监听器"贴"上去。这个过程就是水合。

没有水合,SSR 页面就是一张漂亮的静态截图,而不是一个可用的应用。


三、水合的具体流程

以 Next.js 的一个客户端组件为例:

javascript 复制代码
'use client';

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      点击次数:{count}
    </button>
  );
}

这个组件在请求时会经历以下几个阶段:

1. 服务端渲染(SSR)

服务器执行组件函数,生成 HTML:

css 复制代码
<button>点击次数:0</button>

注意:此时 onClick 事件不存在 ,因为事件监听器无法被序列化成 HTML。

同时,组件状态 count 的初始值 0 被渲染到了 HTML 里。

2. 客户端加载 JS

浏览器收到 HTML 后,会并行下载对应的客户端 JS 包(包含 React 运行时和该组件的代码)。

3. 水合(Hydration)

JS 加载完成后,React 在客户端重新执行一遍组件函数 ,构建出一棵虚拟 DOM 树。

然后,React 将这棵虚拟 DOM 树与服务器生成的真实 DOM 进行对比(这个过程叫 hydrateRoot)。

如果两者结构一致,React 就会:

  • 保留服务器生成的 DOM 节点(不重新创建)
  • 附加事件监听器(onClick 等)
  • 恢复组件状态(useState 等)

从此,按钮就可以点击了,count 状态也能正常更新。

4. 交互阶段

用户点击按钮 → 触发 onClick → setCount 更新状态 → React 在客户端重新渲染 → 只更新变化的部分。

关键点:水合只做一次,发生在页面首次加载时。 之后的所有更新都是客户端正常的 React 渲染。


四、水合与 CSR 的区别

很多同学会把水合和 CSR 搞混,觉得"都要在客户端执行 JS,有什么区别?"

区别很大:

渲染方式 客户端初始 DOM React 在客户端的动作
CSR 空 #root 从零创建整个 DOM 树
SSR + 水合 服务器生成的完整 HTML 复用已有 DOM,只附加事件

CSR 就像给你一袋面粉和馅料,让你自己包、自己煮 。

水合则是餐厅已经煮好饺子端上来,你只需要蘸醋吃。

水合比 CSR 更快,因为浏览器不用等待 JS 下载执行后才开始渲染页面,HTML 已经在首屏显示出来了。水合只是"锦上添花"地添加交互。


五、水合失败:最常见的坑

水合有一个严格的要求:服务器渲染的 HTML 必须和客户端第一次渲染的虚拟 DOM 完全一致 。

如果不一致,React 就会报错或产生奇怪的行为。

看一个经典的错误示例:

javascript 复制代码
'use client';

export default function BadComponent() {
  return (
    <div>
      当前时间:{new Date().toLocaleTimeString()}
    </div>
  );
}

为什么会出问题?

  • 服务器渲染时,new Date() 是服务器时间,比如 12:00:00。
  • 客户端水合时,new Date() 是浏览器本地时间,比如 12:00:01(甚至不同时区)。
  • 两者不一致,React 发现虚拟 DOM 和真实 DOM 不匹配,就会报错:
sql 复制代码
Hydration failed because the initial UI does not match what was rendered on the server.

解决方法:

  1. 把动态内容放到 useEffect 里,水合完成后再更新:
javascript 复制代码
'use client';

import { useEffect, useState } from 'react';

export default function GoodComponent() {
  const [time, setTime] = useState('');

  useEffect(() => {
    setTime(new Date().toLocaleTimeString());
  }, []);

  return <div>当前时间:{time}</div>;
}
  1. 使用 suppressHydrationWarning 属性(不推荐,治标不治本)。

水合失败的常见原因:

  • 使用了 Math.random()、Date.now() 等不确定值
  • 直接访问 window、localStorage 等浏览器 API
  • 使用了浏览器特有的差异(如不同时区、不同语言)
  • 服务器和客户端的依赖版本不一致

记住:水合要求服务器和客户端第一次渲染结果完全一致。任何不确定的东西,都放到 useEffect 里去做。


六、Next.js 中的水合策略

Next.js 的 App Router 对水合做了很多优化:

1. 服务端组件不参与水合

只有 'use client' 组件才会被水合。服务端组件没有客户端 JS,它们的 HTML 输出后就是最终的静态内容。所以:

  • 服务端组件永远不会水合,也没有事件监听器。
  • 客户端组件先服务端预渲染,再在客户端水合。

2. 选择性水合(Selective Hydration)

React 18 引入了选择性水合 ,可以优先水合用户正在交互的部分,延迟水合其他部分。

比如一个页面有 Header(客户端组件)和 Sidebar(客户端组件),用户先点击了 Header,React 可以优先水合 Header ,让用户立刻得到响应,Sidebar 稍后再水合。

这大大提升了页面的可交互时间(TTI)。

3. 流式 SSR 与水合

Next.js 支持流式渲染 ,服务器可以边渲染边发送 HTML 片段。

浏览器可以更早地开始显示内容,同时客户端 JS 也在并行加载,从而实现边加载边水合。


七、总结

水合是 SSR 和 CSR 之间的桥梁,它让服务端渲染的静态页面具备了客户端交互能力。

  • 概念:水合是 React 将事件监听器和状态附加到服务端 HTML 的过程。
  • 流程:SSR 生成 HTML → 浏览器加载 JS → React 对比虚拟 DOM → 绑定事件 → 页面可交互。
  • 与 CSR 区别:水合是"复用已有 DOM",CSR 是"从零创建 DOM"。
  • 最大坑:服务器和客户端渲染结果必须一致,否则水合失败。
  • Next.js 优化:选择性水合、流式 SSR,让交互更快。

用一个比喻收尾:

服务端渲染是给你端上一盘煮好的水饺,水合是你自己调蘸料、动筷子,把这盘饺子真正吃进肚子里。

下次面试官问你"什么是水合",别再说"就是 hydration",你可以告诉他:水合是让静态 HTML 活过来的过程,是 SSR 和 CSR 之间的黏合剂。

相关推荐
Misnearch3 小时前
React中的回调
前端·javascript·react.js
Java后端的Ai之路9 小时前
09-React组件生命周期
前端·react.js·前端框架
Java后端的Ai之路19 小时前
06-React-Props
前端·react.js·前端框架
zach1 天前
RSC(React Server Components)详解
next.js
Java后端的Ai之路1 天前
07React_Props验证器
人工智能·python·react.js·架构·fastapi
Aolith1 天前
从收藏到错题本,我在 React 全栈项目里实现了一套复习系统
mongodb·react.js·全栈
一木 之林1 天前
Agent智能体学习总结:从一次调用到模型自主规划、工具调用与 ReAct 循环的项目实战-6/6
前端·人工智能·学习·react.js
song5012 天前
React Native for OpenHarmony 实战:三方库 react-native-parallax-scroll-view 的鸿蒙化适配指南
分布式·react native·react.js·transformer·harmonyos
song5012 天前
React Native for OpenHarmony 实战:三方库 react-native-ratings 的鸿蒙化适配指南
javascript·react native·react.js·分类·harmonyos
晓得迷路了2 天前
栗子前端技术周刊第 149 期 - Vite+ 1.0、Preact 11、Next.js 16.4...
前端·javascript·react.js