renderToReadableStream
在现代 JavaScript 环境中将 React 树流式渲染为 Web Streams API
核心概述
renderToReadableStream 是 React 18 引入的服务端渲染 API, 专门用于支持 Web Streams API 的现代 JavaScript 环境,如 Edge Runtime、 Cloudflare Workers、Deno 等。它返回标准的 ReadableStream, 实现流式服务端渲染 (Streaming SSR)。
与 renderToPipeableStream (Node.js 专用) 不同,renderToReadableStream 使用标准的 Web Streams API, 具有更好的跨平台兼容性,可以在任何支持 Web Streams 的环境中运行。
适用场景:
- Cloudflare Workers, Vercel Edge Functions, Deno Deploy
- 任何支持 Web Streams API 的 Edge Runtime
- 使用 Suspense 的数据获取场景
- 需要低延迟的全球边缘部署
🌐 Edge Runtime 优势
- • 全球边缘部署:代码在离用户最近的边缘节点运行
- • 更低的延迟:无需往返源服务器
- • 标准 API:使用 Web Streams,不依赖 Node.js
- • 冷启动快:边缘函数启动时间极短
技术规格
导入方式
import { renderToReadableStream } from 'react-dom/server';函数签名
function renderToReadableStream(
model: ReactNode,
options?: {
bootstrapModules?: string[];
onError?: (error: Error) => void;
identifierPrefix?: string;
nonce?: string;
}
): Promise<ReadableStream>参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
model | ReactNode | 要渲染的 React 元素树 |
options.bootstrapModules | string[] | 客户端 hydration 脚本的 URL 数组 |
options.onError | (error) => void | 错误处理回调 |
返回值
返回一个 Promise<ReadableStream>,该流发出 UTF-8 编码的字节。 可以通过 allReady 方法等待整个应用渲染完成。
实战演练
1. 基础用法:Cloudflare Workers
在 Cloudflare Workers 中实现流式 SSR:
import { renderToReadableStream } from 'react-dom/server';
import App from './App';
export default {
async fetch(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapModules: ['/client.js'],
});
// 等待 shell 渲染完成
await stream.allReady;
return new Response(stream, {
headers: {
'Content-Type': 'text/html',
'Transfer-Encoding': 'chunked',
},
});
},
};2. Vercel Edge Functions
在 Vercel Edge Runtime 中使用:
// app/page.tsx (Edge Runtime)
export const runtime = 'edge';
import { renderToReadableStream } from 'react-dom/server';
import App from './App';
export async function GET(request: Request) {
const stream = await renderToReadableStream(
<App url={request.url} />,
{
bootstrapModules: ['/static/js/client.js'],
onError(error) {
console.error('Rendering error:', error);
},
}
);
return new Response(stream, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
},
});
}3. 使用 Suspense 实现渐进式渲染
结合 Suspense 实现数据获取时的流式传输:
import { Suspense } from 'react';
// 异步数据组件
async function BlogPosts() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
function App() {
return (
<html>
<body>
<div id="root">
<header>网站头部</header>
{/* Suspense 边界:立即显示 fallback,数据加载完成后流式传输实际内容 */}
<Suspense fallback={<div>加载博客文章...</div>}>
<BlogPosts />
</Suspense>
<footer>网站页脚</footer>
</div>
</body>
</html>
);
}
// Edge Runtime 渲染
export async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapModules: ['/client.js'],
onError(error) {
console.error(error);
},
});
// 选项 1:立即开始流式传输 (header 和 footer 立即发送)
return new Response(stream, {
headers: { 'Content-Type': 'text/html' },
});
// 选项 2:等待所有内容渲染完成 (用于爬虫)
// await stream.allReady;
// return new Response(stream, {
// headers: { 'Content-Type': 'text/html' },
// });
}4. 生产级案例:完整的 Edge SSR 实现
包含错误处理、超时和完整 HTML 结构:
import { renderToReadableStream } from 'react-dom/server';
import App from './App';
interface EdgeSSROptions {
request: Request;
}
export async function renderEdgeSSR({ request }: EdgeSSROptions): Promise<Response> {
try {
const stream = await renderToReadableStream(
<App url={request.url} />,
{
bootstrapModules: ['/static/js/client.js'],
onError(error) {
// 记录错误,但不中断流
console.error('Rendering error:', error);
},
}
);
// 创建一个 TransformStream 来包装 HTML
const transformStream = new TransformStream({
start(controller) {
// 在流开始时发送 HTML 头部
controller.enqueue(
new TextEncoder().encode(
'<!DOCTYPE html><html lang="zh-CN"><head>' +
'<meta charset="UTF-8">' +
'<meta name="viewport" content="width=device-width, initial-scale=1.0">' +
'<title>My App</title>' +
'</head><body><div id="root">'
)
);
},
flush(controller) {
// 在流结束时发送 HTML 尾部和 hydration 脚本
controller.enqueue(
new TextEncoder().encode(
'</div>' +
'<script src="/static/js/client.js"></script>' +
'</body></html>'
)
);
},
});
// 连接 React 流和转换流
const transformedStream = stream.pipeThrough(transformStream);
return new Response(transformedStream, {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Transfer-Encoding': 'chunked',
'X-Content-Type-Options': 'nosniff',
},
});
} catch (error) {
console.error('SSR failed:', error);
// 返回错误页面
return new Response(
'<!DOCTYPE html><html><head><title>Error</title></head>' +
'<body><h1>Internal Server Error</h1></body></html>',
{
status: 500,
headers: { 'Content-Type': 'text/html' },
}
);
}
}
// 使用示例
export default {
async fetch(request: Request): Promise<Response> {
return renderEdgeSSR({ request });
},
};避坑指南
陷阱 1: 在 Node.js 环境中使用
问题:Node.js 虽然支持 Web Streams,但应该使用专门的 API。
// ❌ 在 Node.js 中使用 renderToReadableStream
// 虽然可以工作,但性能不如 renderToPipeableStream
// ✅ Node.js 环境:使用 renderToPipeableStream
import { renderToPipeableStream } from 'react-dom/server';
// ✅ Edge Runtime:使用 renderToReadableStream
import { renderToReadableStream } from 'react-dom/server';陷阱 2: 忘记等待 allReady
问题:在需要完整 HTML 的场景(如爬虫)中忘记等待。
// ❌ 错误:立即返回流,Suspense 内容可能未完成 const stream = await renderToReadableStream(<App />); return new Response(stream); // ✅ 正确:等待所有内容渲染完成 const stream = await renderToReadableStream(<App />); await stream.allReady; // 等待 Suspense 内容完成 return new Response(stream);
陷阱 3: 不处理流错误
问题:流可能在渲染过程中因错误而中断。
// ❌ 没有错误处理
const stream = await renderToReadableStream(<App />);
// ✅ 使用 onError 回调处理错误
const stream = await renderToReadableStream(<App />, {
onError(error) {
console.error('Stream error:', error);
// 可以选择将错误信息发送到监控服务
sendToMonitoring(error);
},
});API 对比
| API | 环境 | 流类型 | 使用场景 |
|---|---|---|---|
renderToReadableStream | Edge Runtime | ReadableStream | Cloudflare Workers,Vercel Edge |
renderToPipeableStream | Node.js | stream.Readable | Node.js 服务器 |
renderToString | Node.js | string | 传统 SSR (不推荐) |