在构建现代Web应用时,服务端渲染(SSR)常被视为提升首屏性能的关键手段。但仅仅把页面在服务器上画出来并不够,浏览器还需要知道这些静态节点背后对应的组件逻辑与事件处理函数,这就引出了水合(hydration)机制。与此同时,为了进一步压缩内容到达时间,流式渲染逐渐被主流框架采用。本文围绕这两者展开,说明它们各自的工作方式、代码形态以及组合使用时的注意事项。

什么是水合(Hydration)
水合是指浏览器在接收到服务端输出的HTML后,下载对应的JavaScript包,并重新执行组件代码,把内存中的虚拟DOM与真实DOM节点一一对应起来的过程。只有完成水合,像onClick这样的事件才会真正生效,输入框才能受控。传统SSR框架会在整页HTML发送完毕、且主JS bundle加载完成后,一次性启动水合。
这种方式的实现非常简单,下面是一段基于React的朴素服务端渲染加水合代码:
// server.js 使用 express 与 react-dom/server
const express = require('express');
const React = require('react');
const { renderToString } = require('react-dom/server');
const App = require('./App').default;
const app = express();
app.get('/', (req, res) => {
const html = renderToString(React.createElement(App));
res.send('<!DOCTYPE html><html><body><div id="root">' + html + '</div><script src="/client.js"></script></body></html>');
});
app.listen(3000);
// client.js 水合入口
const React = require('react');
const { hydrateRoot } = require('react-dom/client');
const App = require('./App').default;
hydrateRoot(document.getElementById('root'), React.createElement(App));
上述代码里,服务器调用renderToString生成静态标记,客户端通过hydrateRoot进行水合。优点在于逻辑直观、调试方便;缺点是若页面庞大,用户必须等待全部HTML与JS到位才能开始交互,TTI(可交互时间)会被显著拉长。
流式渲染的工作方式
流式渲染打破了“先出完整HTML再水合”的顺序。服务器利用Node.js的流能力,将组件树分块渲染并立即推送给浏览器。例如React的renderToPipeableStream允许在组件挂起(Suspense)时先发送外层骨架,待数据就绪再补发内部内容。浏览器可渐进解析并显示,不必等末尾标签。
以下示例展示了如何用流式接口输出页面,并在客户端做选择性水合:
// stream-server.js
const express = require('express');
const React = require('react');
const { renderToPipeableStream } = require('react-dom/server');
const App = require('./App').default;
const app = express();
app.get('/', (req, res) => {
res.setHeader('Content-Type', 'text/html');
const stream = renderToPipeableStream(React.createElement(App), {
bootstrapScripts: ['/client.js'],
onShellReady() {
stream.pipe(res);
}
});
});
app.listen(3000);
// client.js 使用 hydrateRoot 同样可对接流式HTML
const React = require('react');
const { hydrateRoot } = require('react-dom/client');
const App = require('./App').default;
hydrateRoot(document.getElementById('root'), React.createElement(App));
在流式模式下,服务器先发送带占位符的外壳,浏览器立刻绘制首屏;内部区块到达后自动插入。配合React 18的 selective hydration,用户点击某块时该块优先级提升,从而缓解长页面整体水合阻塞的问题。需要注意的是,流式渲染要求服务器运行环境支持流式响应,且代理层不能缓冲全部内容。
水合与流式渲染的核心差异
从交付节奏看,传统水合依赖完整文档,流式渲染强调分块到达。从启动时机看,前者通常等onLoad后再统一hydrate,后者允许外壳就绪即开始部分水合。两者并非互斥,流式渲染解决FCP(首次内容绘制),水合解决交互能力,组合使用才能兼顾速度与体验。
| 维度 | 传统水合 | 流式渲染加水合 |
|---|---|---|
| HTML发送 | 整页完成后发送 | 边渲染边发送 |
| 水合启动 | 全部JS加载后 | 外壳或区块到达后即可 |
| 首屏感知 | 较晚 | 较早 |
| 实现复杂度 | 低 | 中高,需流与Suspense配合 |
实际项目中,若页面以静态展示为主、交互少,传统SSR加水合已足够;若含大量动态区块、数据依赖深,采用流式渲染能明显降低白屏时间。无论哪种方案,都应避免在水合前执行重计算,防止主线程卡顿。理解这些机制后,团队可按业务特征灵活选型。
常见误区与排查建议
一个典型误区是认为流式渲染能自动减少JS体积。实际上流只是改变传输顺序,客户端仍需下载并执行同等逻辑才能完成水合。若bundle过大,仍须借助代码分割与懒加载。另一个误区是在水合阶段直接操作未声明节点,这会导致React报出节点不匹配警告。
排查时可在浏览器开发者工具的网络面板观察HTML是否分块到达,并用Performance标签录制TTI。若发现水合长时间占用主线程,应检查是否有同步重渲染或大型列表一次性挂载。通过Suspense边界拆分、使用React.lazy延迟非关键组件,可以有效缩短阻塞。
server_side_renderinghydrationstreaming_render修改时间:2026-08-06 10:45:27