renderToPipeableStream
在 Node.js 环境中将 React 树流式渲染为 Node.js 可读流
核心概述
renderToPipeableStream 是 React 18 引入的服务端渲染 API, 专门用于 Node.js 环境。它允许你将 React 树渲染为 Node.js 的 stream.Readable 流, 实现流式服务端渲染 (Streaming SSR)。
与传统的 renderToString 不同,流式渲染可以渐进式地将 HTML 发送到客户端, 而不是等待整个应用渲染完成。这意味着用户可以更早看到页面的部分内容, 大大改善了首屏加载体验和 Largest Contentful Paint (LCP) 指标。
适用场景:
- Next.js、Remix 等全栈框架的服务端渲染
- 需要流式传输 HTML 的 Node.js 服务器
- 使用 Suspense 的数据获取场景
- 需要优化 Time to First Byte (TTFB) 的应用
💡 流式渲染的优势
- • 更快的首屏显示:部分 HTML 可以立即发送,无需等待整个应用
- • 更好的 Suspense 支持:Suspense 边界外的内容先发送,边界内的内容加载完成后流式传输
- • 更低的 TTFB:首字节时间更短,因为不需要等待所有数据
- • 渐进式增强:关键内容优先,次要内容后加载
技术规格
导入方式
TypeScript
import { renderToPipeableStream } from 'react-dom/server';函数签名
TypeScript
function renderToPipeableStream(
model: ReactNode,
options?: {
bootstrapModules?: string[];
bootstrapScriptContent?: string;
onAllReady?: () => void;
onShellReady?: () => void;
onShellError?: (error: Error) => void;
onError?: (error: Error) => string;
identifierPrefix?: string;
nonce?: string;
}
): PipeableStream参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
model | ReactNode | 要渲染的 React 元素树 |
options | object | 可选的配置选项 |
options.bootstrapModules | string[] | 客户端 hydration 脚本的 URL 数组 |
options.onShellReady | () => void | 当 shell (Suspense 边界外的内容) 渲染完成时调用的回调 |
options.onAllReady | () => void | 当整个应用渲染完成时调用的回调 |
options.onError | (error) => string | 错误处理回调,返回的错误信息会插入到 HTML 中 |
返回值
返回一个 PipeableStream 对象,它是一个 Node.js 可读流, 具有 pipe 方法,可以连接到 Node.js 的 res 对象或其他流。
实战演练
1. 基础用法:Node.js Express 服务器
使用 Express 实现流式 SSR:
TypeScript
import express from 'express';
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';
const app = express();
app.get('/', (req, res) => {
// 渲染应用为流
const stream = renderToPipeableStream(<App />, {
// 当 shell 渲染完成时开始流式传输
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
res.write('<!DOCTYPE html><html><head><title>My App</title></head><body>');
res.write('<div id="root">');
// 将流连接到响应
stream.pipe(res);
},
// 错误处理
onError(error) {
console.error(error);
},
// Bootstrap 脚本
bootstrapModules: ['/client.js'],
});
// 当流结束时关闭 HTML 标签
stream.on('end', () => {
res.end('</div><script src="/client.js"></script></body></html>');
});
});
app.listen(3000);2. 使用 Suspense 实现渐进式渲染
结合 Suspense 实现数据获取时的流式传输:
TypeScript
import { Suspense } from 'react';
// 模拟异步数据获取
async function fetchData() {
await new Promise(resolve => setTimeout(resolve, 2000));
return { name: 'John', email: 'john@example.com' };
}
function UserProfile() {
const data = fetchData(); // 如果是 Promise,Suspense 会等待
return <div>{data.name} - {data.email}</div>;
}
function App() {
return (
<html>
<body>
<div id="root">
<h1>用户中心</h1>
{/* 这部分会立即渲染 */}
<nav>导航菜单</nav>
{/* Suspense 边界:这部分会在数据加载完成后流式传输 */}
<Suspense fallback={<div>加载中...</div>}>
<UserProfile />
</Suspense>
{/* 这部分也会立即渲染 */}
<footer>页脚</footer>
</div>
</body>
</html>
);
}
// 服务端渲染
app.get('/', (req, res) => {
const stream = renderToPipeableStream(<App />, {
onShellReady() {
// 此时 nav 和 footer 已经渲染完成
// UserProfile 的 fallback ("加载中...") 也已发送
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
stream.pipe(res);
},
onError(error) {
console.error(error);
res.statusCode = 500;
res.send('Server Error');
},
});
});3. 生产级案例:完整的服务端渲染实现
包含错误处理、超时和完整 HTML 结构的 SSR 实现:
TypeScript
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';
interface SSRConfig {
res: express.Response;
requestUrl: string;
}
export function renderSSR({ res, requestUrl }: SSRConfig) {
let didError = false;
const stream = renderToPipeableStream(<App url={requestUrl} />, {
// Shell 准备就绪 - 开始流式传输
onShellReady() {
// 如果发生错误,设置错误状态码
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
// 发送 HTML 头部
res.write('<!DOCTYPE html>');
res.write('<html lang="zh-CN">');
res.write('<head>');
res.write('<meta charset="UTF-8">');
res.write('<meta name="viewport" content="width=device-width, initial-scale=1.0">');
res.write('<title>My App</title>');
res.write('</head>');
res.write('<body>');
res.write('<div id="root">');
// 将 React 流连接到响应
stream.pipe(res);
},
// 整个应用渲染完成
onAllReady() {
// 用于爬虫或不需要流式传输的场景
if (!res.headersSent) {
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
stream.pipe(res);
}
},
// Shell 渲染错误
onShellError(error) {
console.error('Shell rendering error:', error);
// 发送错误页面
res.statusCode = 500;
res.setHeader('Content-type', 'text/html');
res.send(`
<!DOCTYPE html>
<html>
<head><title>Server Error</title></head>
<body>
<h1>Something went wrong</h1>
<pre>${error.message}</pre>
</body>
</html>
`);
},
// 错误处理 - 返回堆栈信息用于调试
onError(error) {
didError = true;
console.error('Rendering error:', error);
return error.stack; // 将堆栈信息插入到 HTML 中
},
// 客户端 hydration 脚本
bootstrapModules: ['/static/js/client.js'],
});
// 流结束后关闭 HTML 标签
stream.on('end', () => {
if (!res.writableEnded) {
res.end('</div><script src="/static/js/client.js"></script></body></html>');
}
});
// 超时处理
const timeout = setTimeout(() => {
console.error('Rendering timeout');
stream.abort();
if (!res.headersSent) {
res.statusCode = 504;
res.send('Gateway Timeout');
}
}, 30000); // 30 秒超时
stream.on('error', () => clearTimeout(timeout));
stream.on('end', () => clearTimeout(timeout));
}
// Express 路由
app.get('*', (req, res) => {
renderSSR({ res, requestUrl: req.url });
});避坑指南
陷阱 1: 混淆 onShellReady 和 onAllReady
问题:不理解这两个回调的区别,导致流式渲染失效。
TypeScript
// ❌ 错误:使用 onAllReady 失去了流式传输的优势
const stream = renderToPipeableStream(<App />, {
onAllReady() {
// 这会等待整个应用渲染完成后才发送,包括 Suspense 内的内容
// 等同于 renderToString 的行为
stream.pipe(res);
},
});
// ✅ 正确:使用 onShellReady 实现流式传输
const stream = renderToPipeableStream(<App />, {
onShellReady() {
// Suspense 边界外的内容会立即发送
// Suspense 内的内容会在数据加载完成后流式传输
stream.pipe(res);
},
});核心原则:onShellReady 用于流式传输 (推荐),onAllReady 用于爬虫或不需要流式传输的场景。
陷阱 2: 忘记处理流错误
问题:流可能在传输过程中因网络问题或渲染错误而中断。
TypeScript
// ❌ 错误:没有错误处理
stream.pipe(res);
// ✅ 正确:监听错误事件
stream.on('error', (error) => {
console.error('Stream error:', error);
if (!res.headersSent) {
res.statusCode = 500;
res.send('Internal Server Error');
} else if (!res.writableEnded) {
res.end();
}
});陷阱 3: 在客户端数据获取组件中使用
问题:renderToPipeableStream 只用于服务端,不能在客户端使用。
TypeScript
// ❌ 错误:在浏览器中导入
import { renderToPipeableStream } from 'react-dom/server'; // 运行时错误
// ✅ 正确:只在服务端使用
// server.js
import { renderToPipeableStream } from 'react-dom/server';
// client.js
import { hydrateRoot } from 'react-dom/client';API 对比
| API | 环境 | 流式传输 | 使用场景 |
|---|---|---|---|
renderToPipeableStream | Node.js | ✅ | Node.js 服务器,流式 SSR |
renderToReadableStream | Web 标准 | ✅ | Edge Runtime,Cloudflare Workers |
renderToString | Node.js | ❌ | 传统 SSR,不推荐 |
renderToStaticMarkup | Node.js | ❌ | 静态 HTML (非交互) |