服务端渲染 — 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 边界时,它有两种策略:
- 内容已就绪:直接内联渲染,无需额外边界开销
- 内容挂起:先将 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——useState、useEffect、useReducer 等客户端状态 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— 对应 webpackreact-server-dom-turbopack— 对应 turbopack(Next.js)react-server-dom-parcel— 对应 Parcelreact-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 系统在构建时而非运行时完成这些连接,从而在生产环境中消除了抽象层带来的性能开销。