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>

参数说明

参数类型说明
modelReactNode要渲染的 React 元素树
options.bootstrapModulesstring[]客户端 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环境流类型使用场景
renderToReadableStreamEdge RuntimeReadableStreamCloudflare Workers,Vercel Edge
renderToPipeableStreamNode.jsstream.ReadableNode.js 服务器
renderToStringNode.jsstring传统 SSR (不推荐)

延伸阅读

这篇文章有帮助吗?