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)

  1. 在服务器端运行 React: 当一个请求到达服务器时,服务器端的 Node.js 环境会调用 React 的渲染 API(如 ReactDOMServer.renderToString())来执行 React 组件。
  2. 生成 HTML: React 在服务器内存中构建组件树,并将其渲染为一段 HTML 字符串。
  3. 发送响应: 服务器将这段生成的 HTML 字符串作为响应发送给客户端浏览器。
SSR 的核心优势

由于浏览器直接接收到的是完整的 HTML 内容,它可以立即进行解析和渲染,用户能够非常迅速地看到页面的非交互式内容。这极大地优化了首次内容绘制 (First Contentful Paint, FCP)最大内容绘制 (Largest Contentful Paint, LCP) 等核心性能指标。

阶段二:客户端注水 (Client-Side Hydration)

这是 Hydration 过程的核心。浏览器在接收到 HTML 并开始渲染的同时,也在下载页面所需的 JavaScript 包。

机制:

  1. 当客户端的 JavaScript 加载并执行后,React 会在内存中重新运行整个应用的组件代码,生成一个期望的组件树。
  2. React 不会立即创建新的 DOM 节点,而是会对比内存中生成的组件树结构与服务器返回的现有 HTML 结构。
  3. 如果两棵树的结构(标签类型、属性、层级)完全匹配,React 就会认为服务端的渲染是有效的,并跳过创建 DOM 节点的步骤
  4. 最后,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 />);

水合错误与常见陷阱

Hydration 的黄金法则

客户端在首次渲染时生成的 UI,必须与服务器返回的 HTML 结构完全一致。

任何不匹配都会导致水合错误 (Hydration Error)。在开发模式下,React 会在控制台打印详细的警告;在生产模式下,React 会默认放弃注水,转而执行一次完整的客户端渲染来修正差异,但这会抵消 SSR 带来的性能优势。

常见的不匹配原因:

  • 浏览器独有 API: 在组件的顶层逻辑中使用了仅存在于浏览器的 API,如 window, localStorage
  • 随机性: 在渲染逻辑中使用了 Math.random()new Date() 等每次运行结果都不同的函数。
  • 数据差异: 服务器与客户端获取到的初始数据不一致。
  • 时间戳或时区差异