Skip to content

第8章 流式SSR(Fizz)

本章将深入React 19的流式服务端渲染引擎Fizz,理解Task任务系统、Segment分段渲染、Suspense在SSR中的处理机制。这是理解React全栈架构的关键一章。

在传统的服务端渲染(SSR)中,服务器必须等待整个页面渲染完成后,才能将HTML发送给客户端。如果页面中有慢速的数据获取操作,用户就需要长时间等待白屏。React 18引入了流式SSR,允许服务器边渲染边发送HTML,大大提升了首屏加载速度和用户体验。

为什么需要流式SSR?Fizz是如何实现流式渲染的?Task任务系统如何调度渲染工作?Segment如何分段输出HTML?Suspense在SSR中如何处理异步组件?

本章将逐一解答这些问题,带你深入理解React流式SSR的设计与实现。


8.1 Fizz架构概述

Fizz是React服务端流式渲染引擎的内部代号,它是React 18+服务端渲染的核心实现。

8.1.1 什么是Fizz

Fizz是React的服务端渲染器,负责将React组件树渲染成HTML字符串并通过流式输出发送给客户端。

Fizz的核心特性

  1. 流式输出:边渲染边发送,不需要等待整个页面完成
  2. Suspense支持:可以延迟渲染慢速组件,先发送fallback
  3. 选择性水合:客户端可以优先水合用户交互的部分
  4. 错误恢复:支持错误边界,局部错误不影响整体渲染

为什么叫Fizz?

Fizz这个名字来源于"fizzy"(起泡的),形象地描述了HTML像气泡一样不断从服务器"冒出"并发送给客户端的过程。

8.1.2 Fizz与传统SSR的区别

让我们通过对比来理解Fizz的优势。

传统SSR的问题

jsx
// 传统SSR流程
async function traditionalSSR(App) {
  // 1. 等待所有数据加载
  const data1 = await fetchSlowData1(); // 3秒
  const data2 = await fetchSlowData2(); // 2秒
  
  // 2. 渲染整个页面
  const html = renderToString(<App data1={data1} data2={data2} />);
  
  // 3. 发送完整HTML(总共5秒后)
  res.send(html);
}

// 用户体验:
// 0-5秒:白屏等待
// 5秒:看到完整页面

Fizz流式SSR的优势

jsx
// Fizz流式SSR流程
async function streamingSSR(App) {
  const stream = renderToPipeableStream(<App />, {
    onShellReady() {
      // 1. 立即发送页面骨架(0.1秒)
      stream.pipe(res);
    }
  });
}

// 组件中使用Suspense
function App() {
  return (
    <div>
      <Header /> {/* 立即渲染 */}
      <Suspense fallback={<Spinner />}>
        <SlowComponent1 /> {/* 3秒后渲染 */}
      </Suspense>
      <Suspense fallback={<Spinner />}>
        <SlowComponent2 /> {/* 2秒后渲染 */}
      </Suspense>
    </div>
  );
}

// 用户体验:
// 0.1秒:看到Header和两个Spinner
// 2秒:SlowComponent2替换Spinner
// 3秒:SlowComponent1替换Spinner

对比表格

特性传统SSRFizz流式SSR
首字节时间(TTFB)慢(等待所有数据)快(立即发送骨架)
用户感知长时间白屏渐进式加载
Suspense支持不支持完全支持
错误处理全局失败局部降级
选择性水合不支持支持

8.1.3 核心文件结构

Fizz的核心实现位于packages/react-server/src/目录下。

主要文件

packages/react-server/src/
├── ReactFizzServer.js          # Fizz核心入口
├── ReactFizzThenable.js        # Promise/Thenable处理
├── ReactFizzCache.js           # 服务端缓存
├── ReactFizzHooks.js           # 服务端Hooks实现
├── ReactFizzComponentStack.js  # 组件栈追踪
└── forks/
    ├── ReactFizzConfig.dom-node.js    # Node.js环境配置
    ├── ReactFizzConfig.dom-browser.js # 浏览器环境配置
    └── ReactFizzConfig.dom-edge.js    # Edge环境配置

ReactFizzServer.js的核心导出

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

// Node.js环境的流式渲染
export function renderToPipeableStream(
  children: ReactNodeList,
  options?: Options,
): PipeableStream;

// Web Streams API的流式渲染
export function renderToReadableStream(
  children: ReactNodeList,
  options?: Options,
): Promise<ReadableStream>;

// 静态HTML生成(不支持Suspense)
export function renderToStaticMarkup(
  children: ReactNodeList,
  options?: Options,
): string;

核心数据结构

javascript
// Request:渲染请求对象
type Request = {
  destination: Destination,        // 输出目标(流)
  allPendingTasks: number,        // 待处理任务数
  pendingRootTasks: number,       // 根任务数
  completedRootSegment: null | Segment, // 完成的根Segment
  abortableTasks: Set<Task>,      // 可中止的任务集合
  pingedTasks: Array<Task>,       // 被唤醒的任务
  // ... 其他属性
};

// Task:渲染任务
type Task = {
  node: ReactNodeList,            // 要渲染的React节点
  ping: () => void,               // 唤醒任务的函数
  blockedBoundary: null | SuspenseBoundary, // 阻塞的Suspense边界
  blockedSegment: Segment,        // 阻塞的Segment
  // ... 其他属性
};

// Segment:HTML片段
type Segment = {
  status: SegmentStatus,          // 状态:PENDING/COMPLETED/ERRORED
  id: number,                     // Segment ID
  chunks: Array<Chunk>,           // HTML块数组
  children: Array<Segment>,       // 子Segment
  // ... 其他属性
};

Fizz的工作流程


8.2 renderToPipeableStream源码分析

renderToPipeableStream是Node.js环境下流式SSR的入口函数,它返回一个可管道化的流对象。

8.2.1 函数入口与参数

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:2850-2900(React 19.3.0)

export function renderToPipeableStream(
  children: ReactNodeList,
  options?: Options,
): PipeableStream {
  // 1. 创建Request对象
  const request = createRequest(
    children,
    null, // responseState
    createRootFormatContext(),
    Infinity, // progressiveChunkSize
    options ? options.onError : undefined,
    options ? options.onAllReady : undefined,
    options ? options.onShellReady : undefined,
    options ? options.onShellError : undefined,
    undefined, // onFatalError
    options ? options.onPostpone : undefined,
    options ? options.formState : undefined,
  );
  
  // 2. 标记为已开始
  let hasStartedFlowing = false;
  
  // 3. 开始异步渲染
  startWork(request);
  
  // 4. 返回PipeableStream对象
  return {
    pipe<T: Writable>(destination: T): T {
      if (hasStartedFlowing) {
        throw new Error(
          'React currently only supports piping to one writable stream.'
        );
      }
      hasStartedFlowing = true;
      startFlowing(request, destination);
      destination.on('drain', createDrainHandler(destination, request));
      return destination;
    },
    abort(reason: mixed) {
      abort(request, reason);
    },
  };
}

参数说明

  1. children:要渲染的React元素
  2. options:配置选项
    • onError:错误处理回调
    • onAllReady:所有内容就绪回调
    • onShellReady:Shell就绪回调(推荐使用)
    • onShellError:Shell错误回调
    • onPostpone:延迟渲染回调
    • formState:表单状态(Server Actions)

返回值:PipeableStream

javascript
type PipeableStream = {
  // 将渲染结果管道到可写流
  pipe<T: Writable>(destination: T): T,
  
  // 中止渲染
  abort(reason: mixed): void,
};

8.2.2 Request对象创建

Request对象是Fizz渲染的核心,它保存了渲染的所有状态。

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1150-1250(React 19.3.0,简化版)

function createRequest(
  children: ReactNodeList,
  responseState: ResponseState,
  rootFormatContext: FormatContext,
  progressiveChunkSize: number,
  onError: void | ((error: mixed) => ?string),
  onAllReady: void | (() => void),
  onShellReady: void | (() => void),
  onShellError: void | ((error: mixed) => void),
  onFatalError: void | ((error: mixed) => void),
  onPostpone: void | ((reason: string) => void),
  formState: void | null | FormState,
): Request {
  // 1. 创建根Segment
  const pingedTasks: Array<Task> = [];
  const abortSet: Set<Task> = new Set();
  
  const request: Request = {
    destination: null,
    flushScheduled: false,
    resumableState: createResumableState(
      responseState ? responseState.identifierPrefix : undefined,
      responseState ? responseState.unstable_externalRuntimeSrc : undefined,
    ),
    responseState,
    progressiveChunkSize:
      progressiveChunkSize === undefined ? 12800 : progressiveChunkSize,
    status: OPENING,
    fatalError: null,
    nextSegmentId: 0,
    allPendingTasks: 0,
    pendingRootTasks: 0,
    completedRootSegment: null,
    abortableTasks: abortSet,
    pingedTasks: pingedTasks,
    clientRenderedBoundaries: ([]: Array<SuspenseBoundary>),
    completedBoundaries: ([]: Array<SuspenseBoundary>),
    partialBoundaries: ([]: Array<SuspenseBoundary>),
    trackedPostpones: null,
    onError: onError === undefined ? defaultOnError : onError,
    onPostpone: onPostpone === undefined ? defaultOnPostpone : onPostpone,
    onAllReady: onAllReady === undefined ? noop : onAllReady,
    onShellReady: onShellReady === undefined ? noop : onShellReady,
    onShellError: onShellError === undefined ? noop : onShellError,
    onFatalError: onFatalError === undefined ? noop : onFatalError,
    formState: formState === undefined ? null : formState,
  };
  
  // 2. 创建根Segment
  const rootSegment = createPendingSegment(
    request,
    0,
    null,
    rootFormatContext,
    false,
    false,
  );
  rootSegment.parentFlushed = true;
  
  // 3. 创建根Task
  const rootTask = createRenderTask(
    request,
    null,
    children,
    -1,
    null,
    rootSegment,
    abortSet,
    null,
    rootFormatContext,
    emptyContextObject,
    null,
    emptyTreeContext,
  );
  
  pingedTasks.push(rootTask);
  
  return request;
}

Request对象的关键属性

  1. destination:输出目标(Node.js的Writable流)
  2. status:渲染状态
    • OPENING:正在渲染Shell
    • CLOSING:Shell完成,渲染延迟内容
    • CLOSED:全部完成
  3. allPendingTasks:待处理任务总数
  4. pendingRootTasks:根任务数(Shell相关)
  5. completedRootSegment:完成的根Segment
  6. pingedTasks:被唤醒的任务队列
  7. completedBoundaries:完成的Suspense边界

8.2.3 流式输出机制

Fizz通过Node.js的Stream API实现流式输出。

startFlowing:开始流式输出

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

function startFlowing(request: Request, destination: Destination): void {
  // 1. 设置destination
  request.destination = destination;
  
  // 2. 尝试完成请求
  try {
    flushCompletedQueues(request, destination);
  } catch (error) {
    logRecoverableError(request, error);
    fatalError(request, error);
  }
}

flushCompletedQueues:刷新完成的队列

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:2200-2300(React 19.3.0,简化版)

function flushCompletedQueues(
  request: Request,
  destination: Destination,
): void {
  beginWriting(destination);
  
  try {
    // 1. 刷新完成的根Segment
    const completedRootSegment = request.completedRootSegment;
    if (completedRootSegment !== null && request.pendingRootTasks === 0) {
      flushSegment(request, destination, completedRootSegment);
      request.completedRootSegment = null;
      writeBootstrap(destination, request.resumableState);
    }
    
    // 2. 刷新完成的Suspense边界
    const completedBoundaries = request.completedBoundaries;
    let i = 0;
    for (; i < completedBoundaries.length; i++) {
      const boundary = completedBoundaries[i];
      if (!flushCompletedBoundary(request, destination, boundary)) {
        // 流已满,暂停刷新
        request.destination = null;
        i++;
        completedBoundaries.splice(0, i);
        return;
      }
    }
    completedBoundaries.splice(0, i);
    
    // 3. 刷新部分边界
    const partialBoundaries = request.partialBoundaries;
    i = 0;
    for (; i < partialBoundaries.length; i++) {
      const boundary = partialBoundaries[i];
      if (!flushPartialBoundary(request, destination, boundary)) {
        request.destination = null;
        i++;
        partialBoundaries.splice(0, i);
        return;
      }
    }
    partialBoundaries.splice(0, i);
    
    // 4. 刷新客户端渲染的边界
    const clientRenderedBoundaries = request.clientRenderedBoundaries;
    i = 0;
    for (; i < clientRenderedBoundaries.length; i++) {
      const boundary = clientRenderedBoundaries[i];
      if (!flushClientRenderedBoundary(request, destination, boundary)) {
        request.destination = null;
        i++;
        clientRenderedBoundaries.splice(0, i);
        return;
      }
    }
    clientRenderedBoundaries.splice(0, i);
  } finally {
    completeWriting(destination);
    
    // 5. 检查是否全部完成
    if (
      request.allPendingTasks === 0 &&
      request.pingedTasks.length === 0 &&
      request.clientRenderedBoundaries.length === 0 &&
      request.completedBoundaries.length === 0
    ) {
      close(destination);
    }
  }
}

输出流程图

示例:使用renderToPipeableStream

javascript
// server.js
import { renderToPipeableStream } from 'react-dom/server';
import App from './App';

app.get('/', (req, res) => {
  const { pipe, abort } = renderToPipeableStream(<App />, {
    // Shell就绪时调用(推荐)
    onShellReady() {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
    },
    
    // Shell错误时调用
    onShellError(error) {
      res.statusCode = 500;
      res.send('<h1>Server Error</h1>');
    },
    
    // 所有内容就绪时调用
    onAllReady() {
      console.log('All content ready');
    },
    
    // 错误处理
    onError(error) {
      console.error('Render error:', error);
    },
  });
  
  // 请求超时时中止渲染
  req.setTimeout(10000, () => {
    abort();
  });
});

Shell vs All Ready

jsx
function App() {
  return (
    <html>
      <head>
        <title>My App</title>
      </head>
      <body>
        <Header />  {/* Shell的一部分 */}
        <Suspense fallback={<Spinner />}>
          <SlowContent />  {/* 不是Shell的一部分 */}
        </Suspense>
        <Footer />  {/* Shell的一部分 */}
      </body>
    </html>
  );
}

// 时间线:
// 0.1秒:onShellReady触发
//        - 发送:<Header /> + <Spinner /> + <Footer />
// 3秒:  SlowContent完成
//        - 发送:<SlowContent />的HTML + 替换脚本
// 3秒:  onAllReady触发

Shell的定义

Shell是指不在Suspense边界内的所有内容,包括:

  • HTML结构(html、head、body标签)
  • 不在Suspense内的组件
  • Suspense的fallback

为什么使用onShellReady而不是onAllReady?

  1. 更快的首字节时间:Shell通常很快就能渲染完成
  2. 更好的用户体验:用户立即看到页面结构
  3. SEO友好:搜索引擎可以立即抓取Shell内容
  4. 渐进式加载:延迟内容在后台继续加载
javascript
// ✗ 不推荐:等待所有内容
onAllReady() {
  pipe(res);
}
// 问题:如果有慢速组件,用户需要等待很久

// ✓ 推荐:Shell就绪时立即发送
onShellReady() {
  pipe(res);
}
// 优势:用户立即看到页面,慢速内容渐进加载

8.3 Task任务系统

Fizz使用Task来组织渲染工作,每个Task负责渲染一部分React节点。

8.3.1 Task数据结构

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:350-400(React 19.3.0)

type Task = {
  node: ReactNodeList,                    // 要渲染的React节点
  ping: () => void,                       // 唤醒任务的函数
  blockedBoundary: null | SuspenseBoundary, // 阻塞的Suspense边界
  blockedSegment: Segment,                // 阻塞的Segment
  abortSet: Set<Task>,                    // 可中止任务集合
  keyPath: KeyNode,                       // 组件key路径
  formatContext: FormatContext,           // 格式上下文(HTML/SVG)
  legacyContext: LegacyContext,           // 旧版Context
  context: ContextSnapshot,               // 新版Context快照
  treeContext: TreeContext,               // 树上下文
  componentStack: null | ComponentStackNode, // 组件栈
  thenableState: null | ThenableState,    // Promise状态
};

Task的关键属性

  1. node:要渲染的React节点

    • 可以是元素、组件、文本等
    • 渲染过程中会递归处理子节点
  2. ping:唤醒函数

    • 当异步操作完成时调用
    • 将Task重新加入pingedTasks队列
  3. blockedBoundary:阻塞的Suspense边界

    • 如果Task在Suspense内,指向该Suspense
    • 用于处理Suspense的fallback和内容
  4. blockedSegment:阻塞的Segment

    • Task渲染的目标Segment
    • 渲染结果会写入这个Segment
  5. abortSet:可中止任务集合

    • 用于批量中止相关任务
    • 当请求中止时,所有任务都会被中止

8.3.2 任务队列管理

Fizz维护多个任务队列来管理渲染工作。

任务队列类型

javascript
type Request = {
  // 1. 被唤醒的任务(优先处理)
  pingedTasks: Array<Task>,
  
  // 2. 可中止的任务集合
  abortableTasks: Set<Task>,
  
  // 3. 待处理任务计数
  allPendingTasks: number,      // 所有待处理任务
  pendingRootTasks: number,     // Shell相关的根任务
};

createRenderTask:创建渲染任务

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1300-1350(React 19.3.0,简化版)

function createRenderTask(
  request: Request,
  thenableState: ThenableState | null,
  node: ReactNodeList,
  childIndex: number,
  blockedBoundary: Root | SuspenseBoundary,
  blockedSegment: Segment,
  abortSet: Set<Task>,
  keyPath: KeyNode,
  formatContext: FormatContext,
  legacyContext: LegacyContext,
  context: ContextSnapshot,
  treeContext: TreeContext,
): Task {
  // 1. 增加待处理任务计数
  request.allPendingTasks++;
  
  if (blockedBoundary === null) {
    // 根任务(Shell的一部分)
    request.pendingRootTasks++;
  } else {
    // Suspense内的任务
    blockedBoundary.pendingTasks++;
  }
  
  // 2. 创建Task对象
  const task: Task = {
    replay: null,
    node,
    ping: () => pingTask(request, task),
    blockedBoundary,
    blockedSegment,
    abortSet,
    keyPath,
    formatContext,
    legacyContext,
    context,
    treeContext,
    componentStack: null,
    thenableState,
  };
  
  // 3. 添加到可中止任务集合
  abortSet.add(task);
  
  return task;
}

pingTask:唤醒任务

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

function pingTask(request: Request, task: Task): void {
  // 1. 将任务加入pingedTasks队列
  const pingedTasks = request.pingedTasks;
  pingedTasks.push(task);
  
  // 2. 如果只有一个任务,立即处理
  if (pingedTasks.length === 1) {
    scheduleWork(() => performWork(request));
  }
}

retryTask:重试任务

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1800-1900(React 19.3.0,简化版)

function retryTask(request: Request, task: Task): void {
  const segment = task.blockedSegment;
  
  if (segment.status !== PENDING) {
    // Segment已经完成或出错,不需要重试
    return;
  }
  
  // 切换到Task的上下文
  switchContext(task.context);
  
  let prevTaskInDEV = null;
  if (__DEV__) {
    prevTaskInDEV = currentTaskInDEV;
    currentTaskInDEV = task;
  }
  
  try {
    // 重新渲染节点
    renderNodeDestructive(request, task, task.node, -1);
    
    // 渲染成功,完成Segment
    segment.status = COMPLETED;
    finishedTask(request, task.blockedBoundary, segment);
  } catch (thrownValue) {
    // 处理错误或Suspense
    handleTaskError(request, task, thrownValue);
  } finally {
    if (__DEV__) {
      currentTaskInDEV = prevTaskInDEV;
    }
  }
}

8.3.3 任务执行流程

Fizz通过performWork函数驱动任务执行。

performWork:执行工作

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1700-1800(React 19.3.0,简化版)

function performWork(request: Request): void {
  if (request.status === CLOSED) {
    return;
  }
  
  const prevContext = getActiveContext();
  const prevDispatcher = ReactSharedInternals.H;
  ReactSharedInternals.H = HooksDispatcher;
  
  try {
    const pingedTasks = request.pingedTasks;
    let i = 0;
    
    // 处理所有被唤醒的任务
    for (; i < pingedTasks.length; i++) {
      const task = pingedTasks[i];
      retryTask(request, task);
    }
    
    // 清空已处理的任务
    pingedTasks.splice(0, i);
    
    // 如果有destination,尝试刷新
    if (request.destination !== null) {
      flushCompletedQueues(request, request.destination);
    }
  } catch (error) {
    logRecoverableError(request, error);
    fatalError(request, error);
  } finally {
    ReactSharedInternals.H = prevDispatcher;
    switchContext(prevContext);
  }
}

startWork:开始工作

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

function startWork(request: Request): void {
  scheduleWork(() => performWork(request));
}

function scheduleWork(callback: () => void): void {
  // 使用setImmediate或setTimeout(0)异步执行
  setImmediate(callback);
}

任务执行流程图

示例:任务执行过程

jsx
function App() {
  return (
    <div>
      <Header />
      <Suspense fallback={<Spinner />}>
        <AsyncContent />
      </Suspense>
      <Footer />
    </div>
  );
}

// 任务执行过程:
// 1. 创建根Task
//    - node: <App />
//    - blockedBoundary: null(根任务)
//    - blockedSegment: rootSegment

// 2. 执行根Task
//    - 渲染<div>
//    - 渲染<Header />
//    - 遇到<Suspense>,创建Boundary
//    - 渲染<Spinner />(fallback)
//    - 创建延迟Task渲染<AsyncContent />
//    - 渲染<Footer />
//    - 根Task完成

// 3. Shell完成,触发onShellReady

// 4. <AsyncContent />的Promise resolve
//    - 调用pingTask
//    - 将延迟Task加入pingedTasks

// 5. 执行延迟Task
//    - 渲染<AsyncContent />
//    - 完成Boundary
//    - 刷新Boundary的HTML

// 6. 所有Task完成,触发onAllReady

8.4 Segment分段渲染

Segment是Fizz中HTML片段的基本单位,每个Segment对应一段可以独立输出的HTML。

8.4.1 Segment数据结构

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:250-300(React 19.3.0)

type Segment = {
  status: SegmentStatus,              // 状态
  id: number,                         // Segment ID(用于客户端引用)
  index: number,                      // 在父Segment中的索引
  parentFlushed: boolean,             // 父Segment是否已刷新
  chunks: Array<Chunk>,               // HTML块数组
  children: Array<Segment>,           // 子Segment
  boundary: null | SuspenseBoundary,  // 所属的Suspense边界
  lastPushedText: string,             // 最后推送的文本(用于合并)
  textEmbedded: boolean,              // 文本是否已嵌入
};

// Segment状态
const PENDING = 0;    // 待处理
const COMPLETED = 1;  // 已完成
const FLUSHED = 2;    // 已刷新
const ABORTED = 3;    // 已中止
const ERRORED = 4;    // 出错

Segment的关键属性

  1. status:Segment的状态

    • PENDING:正在渲染
    • COMPLETED:渲染完成,等待刷新
    • FLUSHED:已刷新到客户端
    • ERRORED:渲染出错
  2. id:Segment的唯一标识

    • 用于客户端引用和替换
    • 在Suspense场景中,客户端需要知道替换哪个Segment
  3. chunks:HTML块数组

    • 渲染过程中生成的HTML片段
    • 最终会拼接成完整的HTML字符串
  4. parentFlushed:父Segment是否已刷新

    • 只有父Segment刷新后,子Segment才能刷新
    • 保证HTML的顺序正确

8.4.2 分段的创建与完成

createPendingSegment:创建Segment

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:800-850(React 19.3.0)

function createPendingSegment(
  request: Request,
  index: number,
  boundary: null | SuspenseBoundary,
  parentFormatContext: FormatContext,
  lastPushedText: boolean,
  textEmbedded: boolean,
): Segment {
  return {
    status: PENDING,
    id: -1, // 延迟分配ID
    index,
    parentFlushed: false,
    chunks: [],
    children: [],
    boundary,
    lastPushedText,
    textEmbedded,
  };
}

finishedTask:完成任务

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1400-1500(React 19.3.0,简化版)

function finishedTask(
  request: Request,
  boundary: Root | SuspenseBoundary,
  segment: Segment,
): void {
  // 1. 减少待处理任务计数
  request.allPendingTasks--;
  
  if (boundary === null) {
    // 根任务完成
    request.pendingRootTasks--;
    
    if (request.pendingRootTasks === 0) {
      // 所有根任务完成,Shell就绪
      request.completedRootSegment = segment;
      
      if (request.destination !== null) {
        flushCompletedQueues(request, request.destination);
      }
    }
  } else {
    // Suspense内的任务完成
    boundary.pendingTasks--;
    
    if (boundary.pendingTasks === 0) {
      // Boundary的所有任务完成
      if (boundary.status === PENDING) {
        boundary.status = COMPLETED;
      }
      
      // 将Boundary加入完成队列
      request.completedBoundaries.push(boundary);
      
      if (request.destination !== null) {
        flushCompletedQueues(request, request.destination);
      }
    }
  }
}

flushSegment:刷新Segment

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:2000-2100(React 19.3.0,简化版)

function flushSegment(
  request: Request,
  destination: Destination,
  segment: Segment,
): boolean {
  const boundary = segment.boundary;
  
  if (boundary === null) {
    // 根Segment,直接输出
    return flushSegmentContainer(request, destination, segment);
  } else {
    // Suspense内的Segment,需要特殊处理
    return flushSegmentContainer(request, destination, segment);
  }
}

function flushSegmentContainer(
  request: Request,
  destination: Destination,
  segment: Segment,
): boolean {
  // 1. 标记为已刷新
  segment.status = FLUSHED;
  segment.parentFlushed = true;
  
  // 2. 输出所有chunks
  const chunks = segment.chunks;
  let chunkIdx = 0;
  
  for (; chunkIdx < chunks.length; chunkIdx++) {
    const chunk = chunks[chunkIdx];
    
    if (!writeChunkAndReturn(destination, chunk)) {
      // 流已满,暂停刷新
      segment.chunks.splice(0, chunkIdx);
      return false;
    }
  }
  
  // 3. 清空chunks
  segment.chunks.length = 0;
  
  // 4. 刷新子Segment
  const children = segment.children;
  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    if (!flushSegment(request, destination, child)) {
      return false;
    }
  }
  
  return true;
}

8.4.3 HTML片段的拼接

Fizz通过pushXXX函数将HTML片段添加到Segment的chunks数组中。

pushTextInstance:推送文本

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

function pushTextInstance(
  target: Array<Chunk>,
  text: string,
  renderState: RenderState,
  textEmbedded: boolean,
): boolean {
  if (text === '') {
    return textEmbedded;
  }
  
  if (textEmbedded) {
    // 需要转义HTML特殊字符
    target.push(escapeTextForBrowser(text));
  } else {
    // 第一个文本节点,不需要转义
    target.push(text);
  }
  
  return true;
}

pushStartInstance:推送开始标签

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:3000-3100(React 19.3.0,简化版)

function pushStartInstance(
  target: Array<Chunk>,
  type: string,
  props: Object,
  renderState: RenderState,
  formatContext: FormatContext,
): ReactNodeList {
  // 1. 推送开始标签
  target.push('<', type);
  
  // 2. 推送属性
  for (const propKey in props) {
    if (!hasOwnProperty.call(props, propKey)) {
      continue;
    }
    
    const propValue = props[propKey];
    if (propValue == null) {
      continue;
    }
    
    switch (propKey) {
      case 'children':
        // children单独处理
        break;
      case 'dangerouslySetInnerHTML':
        // innerHTML单独处理
        break;
      case 'style':
        pushStyle(target, propValue);
        break;
      case 'className':
        pushAttribute(target, 'class', propValue);
        break;
      default:
        if (isAttributeNameSafe(propKey)) {
          pushAttribute(target, propKey, propValue);
        }
        break;
    }
  }
  
  // 3. 关闭开始标签
  target.push('>');
  
  // 4. 返回children
  return props.children;
}

pushEndInstance:推送结束标签

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

function pushEndInstance(
  target: Array<Chunk>,
  type: string,
  props: Object,
): void {
  // 自闭合标签不需要结束标签
  if (isVoidElement(type)) {
    return;
  }
  
  target.push('</', type, '>');
}

示例:Segment的构建过程

jsx
function Component() {
  return (
    <div className="container">
      <h1>Hello</h1>
      <p>World</p>
    </div>
  );
}

// Segment构建过程:
// 1. 创建rootSegment
//    chunks: []

// 2. 渲染<div>
//    pushStartInstance(chunks, 'div', { className: 'container' })
//    chunks: ['<div class="container">']

// 3. 渲染<h1>
//    pushStartInstance(chunks, 'h1', {})
//    chunks: ['<div class="container">', '<h1>']
//    pushTextInstance(chunks, 'Hello')
//    chunks: ['<div class="container">', '<h1>', 'Hello']
//    pushEndInstance(chunks, 'h1')
//    chunks: ['<div class="container">', '<h1>', 'Hello', '</h1>']

// 4. 渲染<p>
//    pushStartInstance(chunks, 'p', {})
//    chunks: [..., '<p>']
//    pushTextInstance(chunks, 'World')
//    chunks: [..., '<p>', 'World']
//    pushEndInstance(chunks, 'p')
//    chunks: [..., '<p>', 'World', '</p>']

// 5. 结束<div>
//    pushEndInstance(chunks, 'div')
//    chunks: [..., '</div>']

// 6. 刷新Segment
//    输出:<div class="container"><h1>Hello</h1><p>World</p></div>

Chunk的类型

javascript
// Chunk可以是字符串或Uint8Array
type Chunk = string | Uint8Array;

// 字符串Chunk:直接输出
const chunk1: Chunk = '<div>';

// Uint8Array Chunk:用于二进制数据
const chunk2: Chunk = new Uint8Array([60, 100, 105, 118, 62]); // '<div>'

8.5 Suspense在SSR中的处理

Suspense是流式SSR的核心特性,它允许服务器先发送fallback,等内容就绪后再替换。

8.5.1 Suspense边界识别

当Fizz遇到Suspense组件时,会创建一个SuspenseBoundary对象。

SuspenseBoundary数据结构

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:450-500(React 19.3.0)

type SuspenseBoundary = {
  status: SuspenseBoundaryStatus,     // 状态
  id: number,                         // Boundary ID
  rootSegmentID: number,              // 根Segment ID
  parentFlushed: boolean,             // 父Boundary是否已刷新
  pendingTasks: number,               // 待处理任务数
  completedSegments: Array<Segment>,  // 完成的Segment
  byteSize: number,                   // 字节大小
  fallbackAbortableTasks: Set<Task>,  // fallback的可中止任务
  errorDigest: null | string,         // 错误摘要
  contentState: BoundaryContentState, // 内容状态
  fallbackState: BoundaryFallbackState, // fallback状态
};

// Boundary状态
const PENDING = 0;           // 待处理
const COMPLETED = 1;         // 已完成
const CLIENT_RENDERED = 2;   // 客户端渲染

createSuspenseBoundary:创建Suspense边界

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:900-950(React 19.3.0,简化版)

function createSuspenseBoundary(
  request: Request,
  fallbackAbortableTasks: Set<Task>,
): SuspenseBoundary {
  return {
    status: PENDING,
    id: -1, // 延迟分配ID
    rootSegmentID: -1,
    parentFlushed: false,
    pendingTasks: 0,
    completedSegments: [],
    byteSize: 0,
    fallbackAbortableTasks,
    errorDigest: null,
    contentState: PENDING,
    fallbackState: PENDING,
  };
}

8.5.2 fallback的流式输出

当遇到Suspense时,Fizz会先渲染fallback并立即输出。

renderSuspenseBoundary:渲染Suspense边界

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:4500-4600(React 19.3.0,简化版)

function renderSuspenseBoundary(
  request: Request,
  task: Task,
  props: Object,
): void {
  // 1. 创建Suspense边界
  const boundary = createSuspenseBoundary(
    request,
    new Set(),
  );
  
  // 2. 分配Boundary ID
  const id = (boundary.id = request.nextSegmentId++);
  
  // 3. 推送Suspense容器开始标记
  const target = task.blockedSegment.chunks;
  pushStartCompletedSuspenseBoundary(target, id);
  
  // 4. 创建内容Segment(延迟渲染)
  const contentRootSegment = createPendingSegment(
    request,
    0,
    boundary,
    task.formatContext,
    false,
    false,
  );
  boundary.rootSegmentID = contentRootSegment.id;
  
  // 5. 创建内容Task
  const contentTask = createRenderTask(
    request,
    null,
    props.children, // 实际内容
    -1,
    boundary,
    contentRootSegment,
    boundary.fallbackAbortableTasks,
    task.keyPath,
    task.formatContext,
    task.legacyContext,
    task.context,
    task.treeContext,
  );
  
  // 6. 将内容Task加入队列(延迟处理)
  request.pingedTasks.push(contentTask);
  
  // 7. 渲染fallback(立即输出)
  const fallbackSegment = createPendingSegment(
    request,
    0,
    null,
    task.formatContext,
    false,
    false,
  );
  fallbackSegment.parentFlushed = true;
  
  task.blockedSegment = fallbackSegment;
  
  try {
    // 渲染fallback内容
    renderNode(request, task, props.fallback, -1);
    fallbackSegment.status = COMPLETED;
    
    // 推送fallback到输出
    pushCompletedSegment(
      request,
      task.blockedSegment.chunks,
      fallbackSegment,
    );
  } catch (error) {
    fallbackSegment.status = ERRORED;
    boundary.status = CLIENT_RENDERED;
    boundary.errorDigest = logRecoverableError(request, error);
  }
  
  // 8. 推送Suspense容器结束标记
  pushEndCompletedSuspenseBoundary(target);
}

pushStartCompletedSuspenseBoundary:推送Suspense开始标记

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

function pushStartCompletedSuspenseBoundary(
  target: Array<Chunk>,
  id: number,
): void {
  // 推送一个带有ID的模板标记
  target.push('<!--$?--><template id="B:', id.toString(16), '">');
}

function pushEndCompletedSuspenseBoundary(target: Array<Chunk>): void {
  target.push('</template><!--/$-->');
}

输出的HTML结构

html
<!--$?-->
<template id="B:0">
  <div class="spinner">Loading...</div>
</template>
<!--/$-->

这个HTML结构的含义:

  • <!--$?-->:Suspense边界开始标记
  • <template id="B:0">:fallback内容的容器,ID为0
  • <!--/$-->:Suspense边界结束标记

8.5.3 内容就绪后的替换

当Suspense的内容渲染完成后,Fizz会发送替换脚本。

flushCompletedBoundary:刷新完成的Boundary

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:2400-2500(React 19.3.0,简化版)

function flushCompletedBoundary(
  request: Request,
  destination: Destination,
  boundary: SuspenseBoundary,
): boolean {
  const id = boundary.id;
  
  // 1. 推送隐藏的div容器
  writeChunk(
    destination,
    '<div hidden id="S:' + id.toString(16) + '">',
  );
  
  // 2. 刷新所有完成的Segment
  const completedSegments = boundary.completedSegments;
  for (let i = 0; i < completedSegments.length; i++) {
    const segment = completedSegments[i];
    if (!flushSegment(request, destination, segment)) {
      // 流已满,暂停刷新
      boundary.completedSegments.splice(0, i);
      return false;
    }
  }
  boundary.completedSegments.length = 0;
  
  // 3. 关闭div容器
  writeChunk(destination, '</div>');
  
  // 4. 推送替换脚本
  writeChunk(
    destination,
    '<script>$RC("B:' + id.toString(16) + '","S:' + id.toString(16) + '")</script>',
  );
  
  return true;
}

替换脚本的工作原理

html
<!-- 1. 初始HTML(fallback) -->
<!--$?-->
<template id="B:0">
  <div class="spinner">Loading...</div>
</template>
<!--/$-->

<!-- 2. 内容就绪后,服务器发送 -->
<div hidden id="S:0">
  <div class="content">
    <h1>Loaded Content</h1>
    <p>This is the actual content.</p>
  </div>
</div>
<script>$RC("B:0","S:0")</script>

<!-- 3. 客户端执行$RC函数 -->
<script>
function $RC(boundaryID, contentID) {
  // 找到template和隐藏的div
  const template = document.getElementById(boundaryID);
  const content = document.getElementById(contentID);
  
  // 用实际内容替换template
  const parent = template.parentNode;
  const children = content.children;
  
  // 将content的子节点移动到template的位置
  while (children.length > 0) {
    parent.insertBefore(children[0], template);
  }
  
  // 移除template和隐藏的div
  parent.removeChild(template);
  content.parentNode.removeChild(content);
}
</script>

<!-- 4. 最终HTML -->
<!--$?-->
<div class="content">
  <h1>Loaded Content</h1>
  <p>This is the actual content.</p>
</div>
<!--/$-->

8.5.4 示例:流式SSR的输出过程

让我们通过一个完整的示例来理解Suspense在SSR中的处理。

jsx
// App.js
function App() {
  return (
    <html>
      <head>
        <title>Streaming SSR</title>
      </head>
      <body>
        <Header />
        <Suspense fallback={<Spinner />}>
          <SlowContent />
        </Suspense>
        <Footer />
      </body>
    </html>
  );
}

function Header() {
  return <header>Header</header>;
}

function Footer() {
  return <footer>Footer</footer>;
}

function Spinner() {
  return <div className="spinner">Loading...</div>;
}

async function SlowContent() {
  // 模拟慢速数据获取
  const data = await fetchData(); // 3秒
  return <div className="content">{data}</div>;
}

输出时间线

时间 0ms:客户端发起请求

时间 10ms:服务器开始渲染
- 渲染<Header />
- 遇到<Suspense>,创建Boundary
- 渲染<Spinner />(fallback)
- 创建延迟Task渲染<SlowContent />
- 渲染<Footer />
- Shell完成,触发onShellReady

时间 50ms:发送Shell HTML
html
<!DOCTYPE html>
<html>
<head>
  <title>Streaming SSR</title>
</head>
<body>
  <header>Header</header>
  <!--$?-->
  <template id="B:0">
    <div class="spinner">Loading...</div>
  </template>
  <!--/$-->
  <footer>Footer</footer>
</body>
</html>
时间 3000ms:<SlowContent />的Promise resolve
- pingTask被调用
- 延迟Task加入pingedTasks
- performWork执行Task
- 渲染<SlowContent />
- Boundary完成
- 刷新Boundary的HTML

时间 3010ms:发送内容替换HTML
html
<div hidden id="S:0">
  <div class="content">Loaded Data</div>
</div>
<script>$RC("B:0","S:0")</script>
时间 3010ms:客户端执行替换脚本
- 找到template#B:0和div#S:0
- 用实际内容替换template
- 移除template和隐藏的div

最终HTML:
html
<!DOCTYPE html>
<html>
<head>
  <title>Streaming SSR</title>
</head>
<body>
  <header>Header</header>
  <!--$?-->
  <div class="content">Loaded Data</div>
  <!--/$-->
  <footer>Footer</footer>
</body>
</html>

用户体验对比

传统SSR:
0-3000ms:白屏等待
3000ms:看到完整页面

流式SSR:
0-50ms:白屏等待
50ms:看到Header、Spinner、Footer
3010ms:Spinner替换为实际内容

嵌套Suspense

jsx
function App() {
  return (
    <div>
      <Suspense fallback={<Spinner1 />}>
        <SlowContent1 />
        <Suspense fallback={<Spinner2 />}>
          <SlowContent2 />
        </Suspense>
      </Suspense>
    </div>
  );
}

// 输出顺序:
// 1. Shell:<Spinner1 />
// 2. SlowContent1完成:替换外层Suspense,显示<SlowContent1 />和<Spinner2 />
// 3. SlowContent2完成:替换内层Suspense,显示<SlowContent2 />

8.6 错误处理

Fizz提供了完善的错误处理机制,支持错误边界和客户端降级。

8.6.1 错误边界

当Suspense边界内的组件抛出错误时,Fizz会捕获错误并标记为客户端渲染。

abortTaskSoft:软中止任务

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1600-1700(React 19.3.0,简化版)

function abortTaskSoft(task: Task, request: Request): void {
  const boundary = task.blockedBoundary;
  const segment = task.blockedSegment;
  
  if (segment.status !== PENDING) {
    // Segment已经完成,不需要中止
    return;
  }
  
  // 标记Segment为已中止
  segment.status = ABORTED;
  
  // 完成任务
  finishedTask(request, boundary, segment);
}

erroredTask:任务出错

javascript
// 文件:packages/react-server/src/ReactFizzServer.js
// 行号:1500-1600(React 19.3.0,简化版)

function erroredTask(
  request: Request,
  boundary: Root | SuspenseBoundary,
  segment: Segment,
  error: mixed,
): void {
  // 1. 标记Segment为出错
  segment.status = ERRORED;
  
  if (boundary === null) {
    // 根任务出错,致命错误
    fatalError(request, error);
    return;
  }
  
  // 2. Suspense边界内的错误
  if (boundary.status === PENDING) {
    // 标记Boundary为客户端渲染
    boundary.status = CLIENT_RENDERED;
    
    // 记录错误摘要
    const errorDigest = logRecoverableError(request, error);
    boundary.errorDigest = errorDigest;
    
    // 中止所有相关任务
    const abortableTasks = boundary.fallbackAbortableTasks;
    abortableTasks.forEach(task => abortTaskSoft(task, request));
    abortableTasks.clear();
    
    // 将Boundary加入客户端渲染队列
    request.clientRenderedBoundaries.push(boundary);
  }
  
  // 3. 完成任务
  request.allPendingTasks--;
  
  if (request.allPendingTasks === 0) {
    const onAllReady = request.onAllReady;
    onAllReady();
  }
}

logRecoverableError:记录可恢复错误

javascript
// 文件:packages/react-server/src/ReactFizzServer.js

function logRecoverableError(request: Request, error: mixed): string {
  // 1. 调用用户提供的onError回调
  const errorDigest = request.onError(error);
  
  // 2. 如果没有返回digest,生成一个
  if (errorDigest != null && typeof errorDigest !== 'string') {
    throw new Error(
      'onError returned something with a type other than "string". ' +
      'onError should return a string and may return null or undefined but ' +
      'must not return anything else.'
    );
  }
  
  return errorDigest || '';
}

8.6.2 错误恢复策略

Fizz支持三种错误恢复策略。

1. 客户端渲染(Client Rendering)

当Suspense边界内的组件出错时,Fizz会标记该边界为客户端渲染,让客户端重新渲染该部分。

javascript
// flushClientRenderedBoundary:刷新客户端渲染的边界
function flushClientRenderedBoundary(
  request: Request,
  destination: Destination,
  boundary: SuspenseBoundary,
): boolean {
  // 推送客户端渲染指令
  return writeChunk(
    destination,
    '<script>$RX("B:' + 
    boundary.id.toString(16) + 
    '","' + 
    escapeJSStringsForInstructionScripts(boundary.errorDigest || '') + 
    '")</script>',
  );
}

客户端渲染脚本

html
<script>
function $RX(boundaryID, digest) {
  // 找到Suspense边界
  const template = document.getElementById(boundaryID);
  
  // 标记为需要客户端渲染
  template.dataset.dgst = digest;
  
  // 触发React客户端重新渲染这个边界
  // React会在Hydration时检测到这个标记,并重新渲染
}
</script>

2. 错误边界(Error Boundary)

如果Suspense外层有ErrorBoundary,错误会被捕获并显示错误UI。

jsx
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Error caught:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }
    return this.props.children;
  }
}

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<Spinner />}>
        <ErrorProneComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

3. 致命错误(Fatal Error)

如果根任务出错(Shell渲染失败),Fizz会调用onShellError回调,让开发者决定如何处理。

javascript
const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    res.statusCode = 200;
    pipe(res);
  },
  
  onShellError(error) {
    // Shell渲染失败,返回错误页面
    res.statusCode = 500;
    res.send(`
      <!DOCTYPE html>
      <html>
        <body>
          <h1>Server Error</h1>
          <p>Sorry, something went wrong.</p>
        </body>
      </html>
    `);
  },
  
  onError(error) {
    // 记录错误
    console.error('Render error:', error);
    return 'error-digest-' + Date.now();
  },
});

8.6.3 客户端降级

当服务端渲染失败时,客户端会接管渲染工作。

降级流程

示例:错误处理流程

jsx
function App() {
  return (
    <div>
      <Header />
      <Suspense fallback={<Spinner />}>
        <ErrorProneComponent />
      </Suspense>
      <Footer />
    </div>
  );
}

function ErrorProneComponent() {
  // 模拟错误
  throw new Error('Component error');
  return <div>Content</div>;
}

// 服务端渲染流程:
// 1. 渲染<Header />:成功
// 2. 遇到<Suspense>:创建Boundary
// 3. 渲染<Spinner />:成功(fallback)
// 4. 创建Task渲染<ErrorProneComponent />
// 5. Shell完成,发送HTML:
//    <Header />
//    <!--$?--><template id="B:0"><Spinner /></template><!--/$-->
//    <Footer />

// 6. 执行延迟Task
// 7. <ErrorProneComponent />抛出错误
// 8. erroredTask被调用
// 9. Boundary标记为CLIENT_RENDERED
// 10. 发送客户端渲染脚本:
//     <script>$RX("B:0","error-digest-123")</script>

// 客户端Hydration流程:
// 1. React开始Hydration
// 2. 检测到Boundary#B:0有data-dgst属性
// 3. 跳过服务端HTML
// 4. 客户端重新渲染<ErrorProneComponent />
// 5. 如果仍然出错,触发ErrorBoundary或显示错误

错误处理最佳实践

  1. 使用ErrorBoundary:在Suspense外层包裹ErrorBoundary
  2. 提供onError回调:记录错误日志,返回错误摘要
  3. 优雅降级:确保客户端可以正常渲染
  4. 监控错误率:跟踪客户端渲染的频率
javascript
// 完整的错误处理配置
const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/html');
    pipe(res);
  },
  
  onShellError(error) {
    res.statusCode = 500;
    res.send(renderErrorPage(error));
  },
  
  onError(error) {
    // 记录错误到监控系统
    logger.error('SSR Error:', {
      message: error.message,
      stack: error.stack,
      url: req.url,
    });
    
    // 返回错误摘要(不要暴露敏感信息)
    return hashError(error);
  },
});

8.7 综合案例:实现一个流式SSR服务器

让我们通过一个完整的案例来实践流式SSR的所有知识点。

8.7.1 项目结构

streaming-ssr-demo/
├── package.json
├── server.js              # 服务器入口
├── src/
│   ├── App.jsx           # 根组件
│   ├── components/
│   │   ├── Header.jsx
│   │   ├── Footer.jsx
│   │   ├── Spinner.jsx
│   │   ├── UserProfile.jsx
│   │   ├── Comments.jsx
│   │   └── Recommendations.jsx
│   └── client.jsx        # 客户端入口
└── public/
    └── client.js         # 客户端bundle

8.7.2 服务器实现

javascript
// server.js
import express from 'express';
import { renderToPipeableStream } from 'react-dom/server';
import { App } from './src/App.jsx';

const app = express();

// 静态文件服务
app.use(express.static('public'));

// SSR路由
app.get('/', (req, res) => {
  const { pipe, abort } = renderToPipeableStream(<App />, {
    bootstrapScripts: ['/client.js'],
    
    onShellReady() {
      // Shell就绪,立即发送
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      pipe(res);
    },
    
    onShellError(error) {
      // Shell错误,返回错误页面
      res.statusCode = 500;
      res.send(`
        <!DOCTYPE html>
        <html>
          <body>
            <h1>Server Error</h1>
            <p>Sorry, something went wrong.</p>
          </body>
        </html>
      `);
    },
    
    onAllReady() {
      // 所有内容就绪
      console.log('All content ready');
    },
    
    onError(error) {
      // 错误处理
      console.error('Render error:', error);
      return `error-${Date.now()}`;
    },
  });
  
  // 30秒超时
  req.setTimeout(30000, () => {
    console.log('Request timeout, aborting render');
    abort();
  });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

8.7.3 应用组件

jsx
// src/App.jsx
import { Suspense } from 'react';
import { Header } from './components/Header';
import { Footer } from './components/Footer';
import { Spinner } from './components/Spinner';
import { UserProfile } from './components/UserProfile';
import { Comments } from './components/Comments';
import { Recommendations } from './components/Recommendations';

export function App() {
  return (
    <html>
      <head>
        <title>Streaming SSR Demo</title>
        <link rel="stylesheet" href="/styles.css" />
      </head>
      <body>
        <Header />
        
        <main>
          {/* 用户资料 - 快速加载 */}
          <Suspense fallback={<Spinner text="Loading profile..." />}>
            <UserProfile userId="123" />
          </Suspense>
          
          {/* 评论列表 - 中速加载 */}
          <Suspense fallback={<Spinner text="Loading comments..." />}>
            <Comments postId="456" />
          </Suspense>
          
          {/* 推荐内容 - 慢速加载 */}
          <Suspense fallback={<Spinner text="Loading recommendations..." />}>
            <Recommendations userId="123" />
          </Suspense>
        </main>
        
        <Footer />
      </body>
    </html>
  );
}
jsx
// src/components/Header.jsx
export function Header() {
  return (
    <header className="header">
      <h1>Streaming SSR Demo</h1>
      <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
      </nav>
    </header>
  );
}
jsx
// src/components/Footer.jsx
export function Footer() {
  return (
    <footer className="footer">
      <p>&copy; 2024 Streaming SSR Demo</p>
    </footer>
  );
}
jsx
// src/components/Spinner.jsx
export function Spinner({ text = 'Loading...' }) {
  return (
    <div className="spinner">
      <div className="spinner-icon"></div>
      <p>{text}</p>
    </div>
  );
}

8.7.4 异步组件

jsx
// src/components/UserProfile.jsx
async function fetchUserProfile(userId) {
  // 模拟API调用 - 1秒
  await new Promise(resolve => setTimeout(resolve, 1000));
  return {
    id: userId,
    name: 'John Doe',
    avatar: '/avatar.jpg',
    bio: 'Software Engineer',
  };
}

export async function UserProfile({ userId }) {
  const user = await fetchUserProfile(userId);
  
  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
}
jsx
// src/components/Comments.jsx
async function fetchComments(postId) {
  // 模拟API调用 - 2秒
  await new Promise(resolve => setTimeout(resolve, 2000));
  return [
    { id: 1, author: 'Alice', text: 'Great post!' },
    { id: 2, author: 'Bob', text: 'Thanks for sharing.' },
    { id: 3, author: 'Charlie', text: 'Very helpful!' },
  ];
}

export async function Comments({ postId }) {
  const comments = await fetchComments(postId);
  
  return (
    <div className="comments">
      <h3>Comments ({comments.length})</h3>
      <ul>
        {comments.map(comment => (
          <li key={comment.id}>
            <strong>{comment.author}:</strong> {comment.text}
          </li>
        ))}
      </ul>
    </div>
  );
}
jsx
// src/components/Recommendations.jsx
async function fetchRecommendations(userId) {
  // 模拟API调用 - 3秒
  await new Promise(resolve => setTimeout(resolve, 3000));
  return [
    { id: 1, title: 'React 19 Features', url: '/post/1' },
    { id: 2, title: 'Streaming SSR Guide', url: '/post/2' },
    { id: 3, title: 'Server Components', url: '/post/3' },
  ];
}

export async function Recommendations({ userId }) {
  const recommendations = await fetchRecommendations(userId);
  
  return (
    <div className="recommendations">
      <h3>Recommended for You</h3>
      <ul>
        {recommendations.map(item => (
          <li key={item.id}>
            <a href={item.url}>{item.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

8.7.5 客户端Hydration

jsx
// src/client.jsx
import { hydrateRoot } from 'react-dom/client';
import { App } from './App';

// Hydration
hydrateRoot(document, <App />);

8.7.6 运行效果

时间线

时间 0ms:客户端发起请求

时间 50ms:Shell就绪
- 发送HTML:Header + 3个Spinner + Footer
- 用户看到页面结构

时间 1000ms:UserProfile完成
- 发送UserProfile的HTML
- 客户端执行替换脚本
- 第一个Spinner替换为用户资料

时间 2000ms:Comments完成
- 发送Comments的HTML
- 客户端执行替换脚本
- 第二个Spinner替换为评论列表

时间 3000ms:Recommendations完成
- 发送Recommendations的HTML
- 客户端执行替换脚本
- 第三个Spinner替换为推荐内容
- 所有内容加载完成

网络瀑布图

0ms     50ms    1000ms  2000ms  3000ms
|-------|-------|-------|-------|
|       |       |       |       |
| Shell |       |       |       |
|-------|       |       |       |
        |       |       |       |
        | User  |       |       |
        |-------|       |       |
                |       |       |
                | Comm  |       |
                |-------|       |
                        |       |
                        | Recom |
                        |-------|

HTML输出顺序

html
<!-- 1. Shell(50ms) -->
<!DOCTYPE html>
<html>
<head>
  <title>Streaming SSR Demo</title>
  <link rel="stylesheet" href="/styles.css" />
</head>
<body>
  <header class="header">
    <h1>Streaming SSR Demo</h1>
    <nav><a href="/">Home</a><a href="/about">About</a></nav>
  </header>
  
  <main>
    <!--$?--><template id="B:0">
      <div class="spinner">
        <div class="spinner-icon"></div>
        <p>Loading profile...</p>
      </div>
    </template><!--/$-->
    
    <!--$?--><template id="B:1">
      <div class="spinner">
        <div class="spinner-icon"></div>
        <p>Loading comments...</p>
      </div>
    </template><!--/$-->
    
    <!--$?--><template id="B:2">
      <div class="spinner">
        <div class="spinner-icon"></div>
        <p>Loading recommendations...</p>
      </div>
    </template><!--/$-->
  </main>
  
  <footer class="footer">
    <p>&copy; 2024 Streaming SSR Demo</p>
  </footer>
  
  <script src="/client.js" async></script>
</body>
</html>

<!-- 2. UserProfile(1000ms) -->
<div hidden id="S:0">
  <div class="user-profile">
    <img src="/avatar.jpg" alt="John Doe" />
    <h2>John Doe</h2>
    <p>Software Engineer</p>
  </div>
</div>
<script>$RC("B:0","S:0")</script>

<!-- 3. Comments(2000ms) -->
<div hidden id="S:1">
  <div class="comments">
    <h3>Comments (3)</h3>
    <ul>
      <li><strong>Alice:</strong> Great post!</li>
      <li><strong>Bob:</strong> Thanks for sharing.</li>
      <li><strong>Charlie:</strong> Very helpful!</li>
    </ul>
  </div>
</div>
<script>$RC("B:1","S:1")</script>

<!-- 4. Recommendations(3000ms) -->
<div hidden id="S:2">
  <div class="recommendations">
    <h3>Recommended for You</h3>
    <ul>
      <li><a href="/post/1">React 19 Features</a></li>
      <li><a href="/post/2">Streaming SSR Guide</a></li>
      <li><a href="/post/3">Server Components</a></li>
    </ul>
  </div>
</div>
<script>$RC("B:2","S:2")</script>

8.7.7 性能优化

1. 并行数据获取

jsx
// ✗ 不好:串行获取
async function SlowComponent() {
  const user = await fetchUser();
  const posts = await fetchPosts(user.id);
  const comments = await fetchComments(posts[0].id);
  return <div>...</div>;
}

// ✓ 好:并行获取
async function FastComponent() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments(),
  ]);
  return <div>...</div>;
}

2. 合理划分Suspense边界

jsx
// ✗ 不好:一个大的Suspense
<Suspense fallback={<BigSpinner />}>
  <FastComponent />
  <SlowComponent />
</Suspense>
// 问题:FastComponent也要等SlowComponent

// ✓ 好:多个小的Suspense
<Suspense fallback={<Spinner />}>
  <FastComponent />
</Suspense>
<Suspense fallback={<Spinner />}>
  <SlowComponent />
</Suspense>
// 优势:FastComponent可以先显示

3. 预加载关键数据

jsx
// 在渲染前预加载关键数据
const userPromise = fetchUser();
const postsPromise = fetchPosts();

function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <UserProfile promise={userPromise} />
      <PostList promise={postsPromise} />
    </Suspense>
  );
}

4. 使用缓存

javascript
// 简单的内存缓存
const cache = new Map();

async function fetchWithCache(key, fetcher) {
  if (cache.has(key)) {
    return cache.get(key);
  }
  
  const data = await fetcher();
  cache.set(key, data);
  
  // 5分钟后过期
  setTimeout(() => cache.delete(key), 5 * 60 * 1000);
  
  return data;
}

async function UserProfile({ userId }) {
  const user = await fetchWithCache(
    `user:${userId}`,
    () => fetchUser(userId)
  );
  return <div>{user.name}</div>;
}

8.7.8 监控与调试

1. 性能监控

javascript
// server.js
app.get('/', (req, res) => {
  const startTime = Date.now();
  let shellTime = 0;
  let allReadyTime = 0;
  
  const { pipe } = renderToPipeableStream(<App />, {
    onShellReady() {
      shellTime = Date.now() - startTime;
      console.log(`Shell ready in ${shellTime}ms`);
      
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      res.setHeader('X-Shell-Time', shellTime);
      pipe(res);
    },
    
    onAllReady() {
      allReadyTime = Date.now() - startTime;
      console.log(`All ready in ${allReadyTime}ms`);
      res.setHeader('X-All-Ready-Time', allReadyTime);
    },
    
    onError(error) {
      console.error('Render error:', {
        message: error.message,
        stack: error.stack,
        url: req.url,
        userAgent: req.headers['user-agent'],
      });
      
      // 发送到监控系统
      sendToMonitoring({
        type: 'ssr-error',
        error: error.message,
        url: req.url,
      });
      
      return `error-${Date.now()}`;
    },
  });
});

2. 调试工具

javascript
// 开发环境下的详细日志
if (process.env.NODE_ENV === 'development') {
  const { pipe } = renderToPipeableStream(<App />, {
    onShellReady() {
      console.log('=== Shell Ready ===');
      console.log('Pending tasks:', request.allPendingTasks);
      pipe(res);
    },
    
    onAllReady() {
      console.log('=== All Ready ===');
      console.log('Total time:', Date.now() - startTime, 'ms');
    },
    
    onError(error) {
      console.error('=== Error ===');
      console.error(error);
      return 'dev-error';
    },
  });
}

3. 性能指标

javascript
// 收集性能指标
const metrics = {
  ttfb: 0,           // Time to First Byte
  shellTime: 0,      // Shell渲染时间
  allReadyTime: 0,   // 全部就绪时间
  errorCount: 0,     // 错误数量
  clientRenderCount: 0, // 客户端渲染次数
};

// 定期上报
setInterval(() => {
  sendMetrics(metrics);
  // 重置计数器
  metrics.errorCount = 0;
  metrics.clientRenderCount = 0;
}, 60000); // 每分钟上报一次

本章小结

本章深入讲解了React流式SSR(Fizz)的实现原理,让我们回顾一下关键要点:

Fizz架构

  1. 流式输出:边渲染边发送,不需要等待整个页面完成
  2. Task任务系统:通过Task组织渲染工作,支持异步和优先级
  3. Segment分段渲染:HTML片段的基本单位,支持独立输出
  4. Suspense支持:先发送fallback,内容就绪后替换

核心概念

  1. Request对象:渲染请求的核心,保存所有状态
  2. Task对象:渲染任务,负责渲染一部分React节点
  3. Segment对象:HTML片段,包含chunks数组
  4. SuspenseBoundary对象:Suspense边界,管理fallback和内容

渲染流程

  1. 创建Request:初始化渲染状态
  2. 创建根Task:开始渲染
  3. 遇到Suspense:创建Boundary,渲染fallback
  4. Shell完成:触发onShellReady,发送HTML
  5. 延迟内容完成:发送替换脚本
  6. 所有完成:触发onAllReady

错误处理

  1. 客户端渲染:Suspense边界内的错误标记为客户端渲染
  2. 错误边界:使用ErrorBoundary捕获错误
  3. 致命错误:Shell渲染失败,调用onShellError

最佳实践

  1. 使用onShellReady:立即发送Shell,提升首字节时间
  2. 合理划分Suspense:避免一个大的Suspense阻塞所有内容
  3. 并行数据获取:使用Promise.all并行获取数据
  4. 错误监控:记录错误日志,跟踪客户端渲染频率

思考题

  1. 为什么流式SSR比传统SSR更快?
  2. Task和Segment的关系是什么?
  3. Suspense的fallback是如何被替换的?
  4. 什么情况下会触发客户端渲染?

在下一章中,我们将学习React Server Components(RSC)和Flight协议,这是React 19全栈架构的核心特性。