- Published on
Hydration:SSR 的核心桥梁
在现代 Web 应用中,服务端渲染 (Server-Side Rendering, SSR) 是一种旨在优化首屏加载性能 (FCP/LCP) 和搜索引擎优化 (SEO) 的关键技术。然而,服务器仅能生成静态的 HTML 结构,无法处理客户端的交互。注水 (Hydration) 正是连接这一鸿沟的核心过程。它由 react-dom/client 在浏览器端执行,其核心使命是将 React 的事件监听器和状态“附加”到服务器已渲染的 HTML 骨架上,使其从一个静态文档,“活化”为一个功能齐全、可交互的客户端应用程序。
Hydration 的三阶段生命周期
Hydration 并非一个孤立的事件,而是一个从服务器到客户端的完整生命周期中的关键一环。
阶段一:服务端预渲染 (Server-Side Pre-rendering)
- 在服务器端运行 React: 当一个请求到达服务器时,服务器端的 Node.js 环境会调用 React 的渲染 API(如
ReactDOMServer.renderToString())来执行 React 组件。 - 生成 HTML: React 在服务器内存中构建组件树,并将其渲染为一段 HTML 字符串。
- 发送响应: 服务器将这段生成的 HTML 字符串作为响应发送给客户端浏览器。
由于浏览器直接接收到的是完整的 HTML 内容,它可以立即进行解析和渲染,用户能够非常迅速地看到页面的非交互式内容。这极大地优化了首次内容绘制 (First Contentful Paint, FCP) 和最大内容绘制 (Largest Contentful Paint, LCP) 等核心性能指标。
阶段二:客户端注水 (Client-Side Hydration)
这是 Hydration 过程的核心。浏览器在接收到 HTML 并开始渲染的同时,也在下载页面所需的 JavaScript 包。
机制:
- 当客户端的 JavaScript 加载并执行后,React 会在内存中重新运行整个应用的组件代码,生成一个期望的组件树。
- React 不会立即创建新的 DOM 节点,而是会对比内存中生成的组件树结构与服务器返回的现有 HTML 结构。
- 如果两棵树的结构(标签类型、属性、层级)完全匹配,React 就会认为服务端的渲染是有效的,并跳过创建 DOM 节点的步骤。
- 最后,React 会遍历这棵树,将所有事件监听器(如
onClick)附加到这些已经存在的 HTML 元素上。
阶段三:接管与常规运行 (Takeover and Normal Operation)
一旦注水过程成功完成,React 就完全接管了整个应用程序的控制权。此时的应用与一个纯粹的客户端渲染 (Client-Side Rendered, CSR) 应用无异。所有后续的状态更新、UI 变化和路由跳转,都将由客户端的 React 运行时完全处理,不再与服务器进行整页的交互。
实现与关键 API
在 React 18 及以上版本中,Hydration 通过 hydrateRoot API 来启动。
hydrateRoot 的标准用法服务器端 (server.js):
import { renderToString } from 'react-dom/server';
import App from './App'; // 导入你的根组件
// 假设在一个 Express.js 或类似框架的路由处理器中
function handleRequest(req, res) {
// 1. 使用 renderToString 将 React 组件渲染为 HTML 字符串
const appHtml = renderToString(<App />);
// 2. 将渲染出的 HTML 嵌入到一个完整的 HTML 文档模板中
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<title>React SSR 示例</title>
</head>
<body>
<!-- root 容器中包含了服务器预渲染的内容 -->
<div id="root">${appHtml}</div>
<!-- 引用客户端的 JavaScript 包以进行 hydration -->
<script src="/bundle.js"></script>
</body>
</html>
`;
// 3. 将完整的 HTML 页面作为响应发送给浏览器
res.send(fullHtml);
}
客户端 (client.js):
import { hydrateRoot } from 'react-dom/client';
import App from './App'; // 导入与服务器端完全相同的根组件
// 1. 获取包含服务器渲染内容的根 DOM 节点
const rootElement = document.getElementById('root');
// 2. 使用 hydrateRoot 来“激活”或“注水”服务器渲染的 HTML
// React 会复用现有的 DOM 节点并附加事件监听器
hydrateRoot(rootElement, <App />);
水合错误与常见陷阱
客户端在首次渲染时生成的 UI,必须与服务器返回的 HTML 结构完全一致。
任何不匹配都会导致水合错误 (Hydration Error)。在开发模式下,React 会在控制台打印详细的警告;在生产模式下,React 会默认放弃注水,转而执行一次完整的客户端渲染来修正差异,但这会抵消 SSR 带来的性能优势。
常见的不匹配原因:
- 浏览器独有 API: 在组件的顶层逻辑中使用了仅存在于浏览器的 API,如
window,localStorage。 - 随机性: 在渲染逻辑中使用了
Math.random()或new Date()等每次运行结果都不同的函数。 - 数据差异: 服务器与客户端获取到的初始数据不一致。
- 时间戳或时区差异。