关于 streaming-ssr
面向 React 开发者的流式服务端渲染指南,介绍现代 React API。它说明如何先发送页面外壳,再逐步传输准备就绪的较慢内容。
主要功能
- 01逐步传输 HTML
- 02提前发送页面外壳
- 03支持 Suspense 边界
- 04处理网络背压
- 05React 流式 API 指导
适用场景
- 01减少大型页面的初始绘制延迟
- 02在较慢区块前发送关键页面内容
- 03网络拥塞时保持渲染响应
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ patternsdev/ skills --skill streaming-ssr安装后在 Claude Code 中输入/streaming-ssr确认已加载。
详细介绍
流式服务端渲染
我们可以通过_流式传输_应用的内容,在服务端渲染应用的同时缩短交互时间(Time To Interactive)。无需生成一个包含当前导航所需标记的大型 HTML 字符串,我们可以先发送外壳,再稍后流式传输较慢的部分。客户端一收到第一批 HTML 数据块,就可以开始解析并绘制页面。
现代 React 流式渲染会在 Node 运行时使用 renderToPipeableStream(),或在 Web Stream 运行时使用 renderToReadableStream(),然后在客户端使用 hydrateRoot() 对响应进行水合。
适用场景
- 如果你希望在 HTML 生成时逐步发送,以改善 TTFB 和 FCP,请使用此方式
- 对于大型页面,此方式很有帮助,因为等待完整 HTML 会延迟首次绘制
不适用场景
- 托管环境不支持流式响应时(某些无服务器平台会缓冲完整响应)
- 对于简单的静态页面,如果 HTML 足够小,流式传输不会带来明显改善
- 当技术栈中的中间件或反向代理会缓冲响应,从而抵消流式传输的优势时
说明
- 使用
renderToPipeableStream(React 18+),而不是已弃用的renderToNodeStream - 将流式传输与
Suspense边界结合使用,以便在较慢的部分加载时流式传输部分内容 - 使用
onShellReady回调,在关键外壳准备就绪后开始流式传输 - 使用
onError回调处理流式传输错误
详情
初始 HTML 会与 App 组件的数据块一起发送到响应对象:
<!DOCTYPE html>
<html>
<head>
<title>Cat Facts</title>
<link rel="stylesheet" href="/style.css" />
<script type="module" defer src="/build/client.js"></script>
</head>
<body>
<h1>Stream Rendered Cat Facts!</h1>
<div id="approot"></div>
</body>
</html>
现代 React 在 Node 上使用 renderToPipeableStream 进行流式渲染:
import { renderToPipeableStream } from "react-dom/server";
app.use("*", (request, response) => {
let didError = false;
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ["/build/client.js"],
onShellReady() {
response.statusCode = didError ? 500 : 200;
response.setHeader("Content-Type", "text/html");
pipe(response);
},
onError(error) {
didError = true;
console.error(error);
},
});
});
如果我们使用 renderToString 在服务端渲染 App 组件,就必须等到整个树渲染完成后才能发送响应。采用流式传输时,服务端可以提前刷新外壳,并在较慢的内容准备就绪后继续发送。
概念
与渐进式水合类似,流式传输是另一种可用于提升 SSR 性能的渲染机制。顾名思义,流式传输意味着 HTML 会在生成时分块从 Node 服务端传输到客户端。即使页面很大,客户端也能更早开始接收 HTML“字节”,因此 TTFB 会缩短且相对稳定。所有主流浏览器都会更早开始解析和渲染流式内容或部分响应。由于渲染是渐进式的,因此可以加快 FP 和 FCP。
流式传输能够很好地应对网络背压。如果网络拥塞,无法继续传输更多字节,渲染器会收到信号并暂停流式传输,直到网络恢复畅通。因此,服务端使用的内存更少,也能更灵敏地应对 I/O 状况。这使 Node.js 服务端能够同时渲染多个请求,并防止较重的请求长时间阻塞较轻的请求。结果是,即使在严苛的环境下,网站也能保持响应。
用于流式传输的 React
React 18 引入了现代流式 API:
renderToPipeableStream(element, options),用于 Node.js HTTP 响应。renderToReadableStream(element, options),用于边缘环境等 Web Streams 运行时。
这些 API 支持 Suspense 边界、onShellReady、onAllReady,以及在客户端通过 hydrateRoot() 进行渐进式水合。
外壳准备就绪后,流的输出即可开始发送字节。响应会逐步向客户端发送数据块,同时服务端继续渲染较慢的数据块。
流式 SSR — 优缺点
流式渲染旨在提升 React SSR 的速度,并带来以下优势:
-
性能提升: 服务端开始渲染后不久,首字节就会到达客户端,因此 TTFB 优于 SSR。无论页面大小如何,TTFB 都更稳定。客户端一收到 HTML 就可以开始解析,因此 FP 和 FCP 也更低。
-
处理背压: 流式渲染能够很好地应对网络背压或拥塞,即使在条件严苛时也能让网站保持响应。
-
支持 SEO: 搜索引擎爬虫可以读取流式响应,因此网站可以进行 SEO 优化。
需要注意的是,流式渲染的实现并不是将 renderToString 简单地查找并替换为 renderToPipeableStream()。在某些情况下,适用于 SSR 的代码可能无法直接用于流式渲染:
-
某些框架会使用 server-render-pass 生成标记,并需要在 SSR 片段之前将其添加到文档中。例如,某些框架会动态确定要添加到页面的 CSS,并将其放入前置的
<style>标签中。 -
在某些代码中,会使用
renderToStaticMarkup生成页面模板,并嵌入renderToString调用来生成动态内容。由于这些情况下需要的是与组件对应的字符串,因此无法将其替换为流。例如:
res.write("<!DOCTYPE html>");
res.write(renderToStaticMarkup(
<html>
<head>
<title>My Page</title>
</head>
<body>
<div id="content">
{ renderToString(<MyPage/>) }
</div>
</body>
</html>);
流式渲染和渐进式水合都可以帮助弥合纯 SSR 与 CSR 体验之间的差距。