Read OSS

服务端渲染 — Fizz、Flight 与 React Server Components

高级

前置知识

  • 第 1–5 篇(完整的客户端 React 知识体系)
  • 对 HTTP 流式传输与分块传输编码有基本了解
  • 熟悉 React Server Components 的概念(use client、use server 指令)
  • 具备 webpack 或其他打包工具的基础知识(适用于集成章节)

服务端渲染 — Fizz、Flight 与 React Server Components

React 的服务端渲染方案已从最初简单的 renderToString 演进为三套各具职责的架构。Fizz 负责带 Suspense 支持的流式 HTML 输出;Flight Server 将 React Server Components 序列化为流式传输协议;Flight Client 则在客户端将该协议反序列化还原为 React 元素。三者协同构成了 RSC 模型——部分组件仅在服务端运行,部分仅在客户端运行,由框架统一协调两侧的边界。

在这最后一篇文章中,我们将全面梳理这三套架构,深入探讨 hooks 在服务端的差异化行为,追踪 Flight 序列化协议的细节,并理解打包工具(webpack、turbopack)如何处理 'use client''use server' 指令。

三种服务端架构概览

graph TD
    subgraph "Server"
        Fizz["Fizz<br/>(react-server/ReactFizzServer)<br/>Renders components → HTML stream"]
        Flight["Flight Server<br/>(react-server/ReactFlightServer)<br/>Renders RSC → JSON-like stream"]
    end

    subgraph "Client"
        FlightClient["Flight Client<br/>(react-client/ReactFlightClient)<br/>Deserializes RSC stream → React elements"]
        Reconciler["Fiber Reconciler<br/>(react-reconciler)<br/>Reconciles elements → DOM"]
    end

    Fizz -->|"HTML stream"| Browser["Browser DOM"]
    Flight -->|"RSC payload"| FlightClient
    FlightClient -->|"React elements"| Reconciler
    Reconciler --> Browser

Fizz 是传统 SSR 的增强版本。它将 React 组件树渲染为 HTML 流,但与旧版 renderToString 不同,它能处理 Suspense 边界——先立即输出 fallback HTML,待数据就绪后再将完整内容流式送出,并附上内联 <script> 标签,在浏览器中完成内容的原位替换。

Flight Server 不生成 HTML。它渲染 Server Components,并将其输出序列化为流式协议。其中,Client Component 的引用以不透明引用的形式保留(不在服务端渲染),最终输出是由 React 元素、客户端引用、序列化 props、Suspense 边界等组成的类型化数据块序列。

Flight Client 接收这条数据流,重建 React 元素树,并借助打包工具清单将客户端组件引用解析为真实的组件函数。最终产物直接输入常规的 Fiber 协调器进行处理。

Fizz — 流式服务端渲染

Fizz 拥有独立的工作循环,与 Fiber 协调器完全解耦。核心逻辑位于 ReactFizzServer.js——超过 5000 行代码,构成了一套完整的渲染引擎。

performWork 函数驱动 Fizz 的渲染过程:

export function performWork(request: Request): void {
  if (request.status === CLOSED || request.status === CLOSING) {
    return;
  }
  const prevContext = getActiveContext();
  const prevDispatcher = ReactSharedInternals.H;
  // ... install Fizz's hooks dispatcher, then render
}

Fizz 在每个工作块中同步处理组件树,边处理边生成 HTML 片段。遇到 Suspense 边界时,它有两种策略:

  1. 内容已就绪:直接内联渲染,无需额外边界开销
  2. 内容挂起:先将 fallback HTML 原位输出,继续渲染其他内容;数据就绪后,将完整 HTML 连同内联 <script> 一起流式送出,替换原有 fallback

正是这种乱序流式输出使 Fizz 具备强大的能力——浏览器立即开始渲染 fallback,随着数据陆续到达,页面逐步完善。

DOM 专用的 Fizz 入口位于 ReactDOMFizzServerBrowser.js

function renderToReadableStream(
  children: ReactNodeList,
  options?: Options,
): Promise<ReactDOMServerReadableStream> {
  return new Promise((resolve, reject) => {
    // ... creates Fizz request, starts work, returns ReadableStream
  });
}

提示: Fizz 与 Fiber 协调器是完全独立的两套渲染引擎。Fizz 不创建 fiber,不使用第 3 篇中的工作循环,也不依赖 lane 系统。它有自己的任务模型、自己的上下文栈以及自己的 hooks dispatcher。两者共享 react-server 包,仅仅是因为都需要渲染组件。

服务端 Hooks — 不同的 Dispatcher

正如第 4 篇所介绍的,hooks 通过 dispatcher 模式工作。在服务端,会安装不同的 dispatcher,只实现 hook API 的子集

Fizz 的 hooks 实现位于 ReactFizzHooks.js,Flight 的 hooks 实现位于 ReactFlightHooks.js

Hook Fizz(SSR) Flight(RSC)
useState ✅ 可用(仅初始状态,不触发更新) ❌ 不可用
useReducer ✅ 可用(仅初始状态) ❌ 不可用
useEffect ⏭️ 空操作(effects 仅在客户端执行) ❌ 不可用
useLayoutEffect ⏭️ 空操作(开发模式下会警告) ❌ 不可用
useRef ✅ 可用(返回 {current}) ❌ 不可用
useMemo ✅ 可用 ✅ 可用
useCallback ✅ 可用 ✅ 可用
useContext ✅ 可用 ✅ 可用
useId ✅ 可用(生成确定性 ID) ✅ 可用
use ✅ 可用(可 await Promise) ✅ 可用

ReactServer.js 入口点只导出在服务端上下文中可用的 hooks——useStateuseEffectuseReducer 等客户端状态 hooks 均不包含在内:

export {
  Children, Activity, Fragment, Profiler, StrictMode,
  Suspense, ViewTransition,
  cloneElement, createElement, createRef, use,
  forwardRef, isValidElement, lazy, memo,
  cache, cacheSignal,
  useId, useCallback, useDebugValue, useMemo,
  version, captureOwnerStack,
};

这一限制由 package.json 中的 react-server 导出条件强制执行——当打包工具在服务端解析 react 时,拿到的就是这个受限的 API 面。

Flight Server — 序列化 React Server Components

Flight Server 负责渲染 Server Components 并生成流式协议。其核心逻辑在于:当遇到客户端组件(即标有 'use client' 的组件)时,它的处理方式如下:

sequenceDiagram
    participant FS as Flight Server
    participant SC as Server Component
    participant CC as Client Component Reference

    FS->>SC: Render <ServerComponent />
    SC-->>FS: Returns <div><ClientComponent name="Alice" /></div>
    FS->>FS: Serialize <div> as React element
    FS->>CC: Encounter ClientComponent
    Note over FS,CC: Don't render! Serialize as reference
    FS->>FS: Emit: {type: "client-ref", id: "module123", props: {name: "Alice"}}

Server Components 在服务端完整执行——函数体运行,useMemo/useCallback/useContext 等 hooks 触发,输出被序列化。但 Client Components 不会被渲染——它们以不透明引用的形式出现在数据流中,同时携带其 props(props 必须可序列化)。

传输协议由一系列换行符分隔的数据块构成,每个块带有类型标记和 ID,块之间通过 ID 相互引用,以增量方式构建出完整的树结构。Suspense 边界形成自然的分块点——当某个 Server Component 挂起时,Flight Server 可以先输出"pending"块,待数据就绪后再替换为已解析的内容。

从 Server Components 传递给 Client Components 的 props 必须能通过 React 的序列化格式处理——支持 JSON 原始类型、React 元素、客户端引用、服务端引用、Promise 及其他结构化类型。函数(Server Actions 除外)不能作为 props 跨边界传递。

Flight Client — 反序列化 RSC 数据流

Flight Client 接收流式协议并重建 React 元素。它在数据块陆续到达时逐块处理,解析引用并逐步构建组件树。

flowchart TD
    Stream["RSC Stream<br/>(chunks arriving over time)"]
    Parse["Parse chunk type + ID"]
    
    Parse -->|"Element chunk"| RE["Create React element"]
    Parse -->|"Client reference"| CR["Resolve via bundler manifest<br/>requireModule(moduleId)"]
    Parse -->|"Pending chunk"| SP["Create Suspense-wrapped promise"]
    Parse -->|"Resolved chunk"| RP["Resolve pending promise<br/>Trigger re-render"]

    RE --> Tree["React Element Tree"]
    CR --> Tree
    SP --> Tree
    RP --> Tree

    Tree --> Reconciler["Feed to Fiber Reconciler"]

当 Flight Client 遇到客户端引用块时,它通过打包工具的模块清单将引用解析为真实的组件函数。这里正是打包工具集成的关键所在——服务端与客户端必须在模块 ID 上保持一致。

待处理的块与 Suspense 无缝集成。当 Flight Client 遇到引用了未解析 Promise 的块时,会创建一个 thenable 供协调器挂起等待。Flight Server 稍后流式送出已解析的块后,Promise 随即 resolve,触发重新渲染并拉取新内容。

Flight Client 处理的最终产物是一棵普通的 React 元素树——与本地 JSX 生成的结果别无二致。这棵树直接输入 Fiber 协调器(与第 3 篇中相同的工作循环),与当前 DOM 进行协调。

打包工具集成 — Webpack 与 Turbopack

RSC 模型需要与打包工具进行深度集成。'use client''use server' 指令由打包插件转换为 Flight 协议可序列化的引用对象

React 为不同打包工具提供了专用包:

  • react-server-dom-webpack — 对应 webpack
  • react-server-dom-turbopack — 对应 turbopack(Next.js)
  • react-server-dom-parcel — 对应 Parcel
  • react-server-dom-esm — 对应原生 ESM

每个包都提供服务端和客户端两个入口。webpack 服务端入口将 Flight Server 与 webpack 特定的模块解析机制对接,而客户端入口则将 Flight Client 与 webpack 的 chunk 加载机制对接。

graph TD
    subgraph "Build Time"
        UC["'use client' directive"] --> BP["Bundler Plugin"]
        US["'use server' directive"] --> BP
        BP --> CM["Client Manifest<br/>(moduleId → chunk mapping)"]
        BP --> SM["Server Manifest<br/>(actionId → handler mapping)"]
    end

    subgraph "Runtime (Server)"
        FSR["Flight Server"] --> CM
        FSR -->|"serializes client refs"| Stream["RSC Stream"]
    end

    subgraph "Runtime (Client)"  
        Stream --> FCL["Flight Client"]
        FCL --> CM2["Client Manifest"]
        CM2 -->|"resolves refs to modules"| Components["Loaded Components"]
    end

客户端引用是形如 { $$typeof: REACT_CLIENT_REFERENCE, $$id: "app/Button.js#default" } 的对象。当 Flight Server 将其作为组件类型遇到时,不会尝试渲染,而是将引用序列化。在客户端,Flight Client 通过打包工具清单解析 $$id,找到对应的 webpack chunk 和导出名称。

服务端引用的方向则相反。当 Server Action 作为 prop 传递给 Client Component 时,它会变为一个引用,客户端可以回调该引用——通常是向服务端发送 HTTP POST 请求,服务端再将引用解析为原始函数。

提示: RSC 架构中,服务端和客户端使用的是同一个 react 包,但导出条件不同。在服务端,import React from 'react' 会通过 react-server 条件解析到 ReactServer.js,该文件只导出服务端兼容的 API。这从根本上防止了在 Server Component 中误用 useState 的情况——这个导出压根不存在。

全局视图

纵观这六篇文章,我们完整地追踪了 React 的架构脉络:从 monorepo 结构与构建系统,到表示组件的 Fiber 数据结构,再到处理 fiber 的工作循环、赋予组件状态的 hooks 系统、连接 DOM 的 host config,最终来到在内容到达浏览器之前完成渲染的服务端架构。

graph TD
    subgraph "Your Code"
        JSX["JSX Components"]
    end

    subgraph "react package"
        API["Public API<br/>createElement, hooks stubs"]
        SI["SharedInternals.H<br/>(dispatcher bridge)"]
    end

    subgraph "react-reconciler"
        Fiber["Fiber Tree<br/>(double-buffered)"]
        WL["Work Loop<br/>(beginWork ↓ completeWork ↑)"]
        Hooks["Hook Implementations<br/>(linked list on fiber)"]
        Lanes["Lane Model<br/>(31-bit priority)"]
        Commit["Commit Phase<br/>(3 sub-phases)"]
    end

    subgraph "react-dom-bindings"
        HC["Host Config<br/>(createInstance, commitUpdate)"]
        Events["Event System<br/>(delegation, SyntheticEvent)"]
    end

    subgraph "react-server"
        Fizz["Fizz SSR"]
        Flight["Flight RSC"]
    end

    JSX --> API
    API --> SI
    SI --> Hooks
    Hooks --> Fiber
    WL --> Fiber
    Lanes --> WL
    WL --> Commit
    Commit --> HC
    HC --> Events
    JSX --> Fizz
    JSX --> Flight

每个部分都紧密相连:fork 系统让构建流水线能够生成针对不同环境的专用 bundle;Fiber 数据结构在渲染过程中承载状态;lane 模型对工作进行优先级排序;工作循环高效处理 fiber;hooks 通过 dispatcher 桥将状态存储在 fiber 上;host config 则将抽象操作转化为真实的 DOM 变更。

React 的架构是工业级关注点分离的典范。协调器不了解 DOM;react 包不了解协调器;Scheduler 不了解 React。每个模块通过简洁、定义明确的接口相互通信——而 fork 系统在构建时而非运行时完成这些连接,从而在生产环境中消除了抽象层带来的性能开销。