深入理解 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. 交互阶段

用户点击按钮 → 触发 onClicksetCount 更新状态 → 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() 等不确定值
  • 直接访问 windowlocalStorage 等浏览器 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 之间的黏合剂。

相关推荐
小林ixn1 小时前
Next.js 全栈实战:从 SPA 到 SSR,SEO 友好到底怎么落地?
react.js·前端框架·next.js
烬羽1 小时前
彻底搞懂 App Router 约定式路由:文件放对位置,路由就自动生成了
react.js·next.js·前端工程化
meilindehuzi_a2 小时前
从 Vite 到 Axios 与 Mock:React Todos 全栈项目架构及请求链路详解
前端·react.js·架构
@卓越俊逸_角立杰出@2 小时前
java实现Agent+ReAct demo(Spring Boot + Spring AI Alibaba ReAct Agent)
java·spring·react.js
用户69190268133917 小时前
用浏览器 WebGPU跑DPSK大模型(1) - 模型的下载和前端下载进度的显示
javascript·react.js·架构
光影少年18 小时前
react navite本地存储:AsyncStorage、MMKV、文件存储
前端·react native·react.js
JieE21221 小时前
前端不等人:用 Mock 接口工程让 React 应用独立起飞
前端·react.js·面试
王林不想说话21 小时前
React + Ant Design 后台项目:63 个业务组件的分层实践
react.js·typescript·ant design
不好听6131 天前
React 记忆化三兄弟:memo、useMemo、useCallback 到底在缓存什么
react.js