13|React Server Components(RSC)在仓库中的落点与边界
本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从运行时到核心算法的关键设计。开源仓库:https://github.com/facebook/react
在第 12 篇我们讲了 Fizz(流式 SSR):
- SSR 的"流式"不是把 HTML 字符串
write进 stream,而是一套可回压(backpressure)的队列系统 Task负责推进渲染(work),Segment/Boundary负责组织写出(flush)
文章末尾我留了一个预告:第 13 篇进入 React Server Components(RSC)。
这篇会回答一个经常让读源码的人卡壳的问题:
packages/react-server/在仓库里到底是什么?它是不是"服务端渲染器"?- RSC 到底是"协议"还是"渲染器"?为什么还要一个
react-server-dom-webpack? - 它与 Fizz(SSR)/Hydration 的契约边界到底在哪:哪些共用 Server 基建,哪些是完全不同的链路?
本文核心文件:
packages/react-server/src/ReactFlightServer.jspackages/react-client/src/ReactFlightClient.jspackages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.jspackages/react-server-dom-webpack/src/ReactFlightWebpackReferences.jspackages/react-server/src/ReactFlightReplyServer.jspackages/react-client/src/ReactFlightReplyClient.jspackages/react-server/src/ReactFlightActionServer.jspackages/react-server-dom-webpack/package.json
0) 先立一个"非直觉结论":RSC 的核心不是 HTML,而是一个可流式传输的"组件模型协议"(Flight)
当你把 RSC 当成"SSR 的升级版",很多概念都会纠缠在一起:
- SSR:服务端把组件树渲染成 HTML(Fizz),客户端拿 HTML 做 hydration
- RSC:服务端把组件树渲染成一份"可被客户端还原"的模型(Flight payload),客户端消费这份模型
在仓库里这种边界非常清晰:
- Fizz(HTML SSR)在
react-server/src/ReactFizzServer.js - RSC/Flight 在
react-server/src/ReactFlightServer.js与react-client/src/ReactFlightClient.js
所以你可以用一句更准确的话来记:
RSC 更像"React 的组件协议 + 运行时",而不是"ReactDOMServer 的另一个 API"。
1) 先看仓库分层:为什么会同时存在 react-server、react-client、react-server-dom-webpack?
1.1 react-server:Flight 的"服务端端点",负责把模型编码成字节流
你打开 packages/react-server/src/ReactFlightServer.js,会看到它做的事情和 Fizz 很像,但产物完全不同:
- 有自己的
Request - 有自己的
Task - 有自己的
startWork / startFlowing / stopFlowing - flush 的判断依然通过
writeChunkAndReturn来尊重 backpressure
区别在于:Fizz flush 的主要内容是 DOM/HTML 指令集;Flight flush 的主要内容是"行协议(rows)"。
1.2 react-client:Flight 的"客户端端点",负责把字节流解码为可用的 React 模型
Flight 的 consumer 并不一定是浏览器:
- 可以是浏览器里的 RSC runtime
- 也可以是服务端自己(比如解码 action 的 bound args、或者 server 作为 client 消费 reply)
这也是为什么 Flight 的 client 运行时被放到 packages/react-client/src/ReactFlightClient.js。
在 ReactFlightClient.js 里你会看到它维护一套"Chunk 状态机"(pending/blocked/resolved_model/resolved_module/fulfilled/rejected),通过解析流中的 rows,把引用、模块、错误逐步拼装起来。
1.3 react-server-dom-webpack:Flight 协议的 DOM + Bundler 绑定层(不是协议本身)
react-server-dom-webpack/package.json 的描述非常直白:
React Server Components bindings for DOM using Webpack. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.
它解决的是"协议之外"的现实问题:
- 如何把 Client Component/Server Action 变成可被序列化的引用?(靠 bundler 产物映射)
- 如何在 Node/WebStreams/Edge 环境把 Flight Request 接到具体 stream API? (
renderToPipeableStream/renderToReadableStream)
换句话说:
react-server/react-client是协议运行时react-server-dom-webpack是一个"把运行时挂到 Webpack 生态的适配器"
2) 入口 API:renderToPipeableStream(Flight)长得像 Fizz,但它写出去的是"协议行"
文件:packages/react-server-dom-webpack/src/server/ReactFlightDOMServerNode.js
你会看到它也提供了一个 Node 风格入口:
renderToPipeableStream(model, webpackMap, options)
其结构几乎复刻了 Fizz 的 pipeable stream:
createRequest(...)startWork(request)pipe(destination)→startFlowing(request, destination)- 监听
drain/error/close,在drain时再startFlowing
关键片段(省略类型,强调结构):
js
const request = createRequest(model, webpackMap, ...);
let hasStartedFlowing = false;
startWork(request);
return {
pipe(destination) {
if (hasStartedFlowing) throw new Error(...);
hasStartedFlowing = true;
startFlowing(request, destination);
destination.on('drain', () => startFlowing(request, destination));
destination.on('error', () => { stopFlowing(request); abort(request, new Error(...)); });
...
return destination;
},
abort(reason) { abort(request, reason); },
};
这段相似性不是巧合:Fizz 与 Flight 都在用 React 自己抽象出来的"可回压写出模型"(host config 里的 writeChunkAndReturn)。
但注意一个本质差异:
- Fizz 的
children是 ReactNode → 目标是 HTML - Flight 的
model是ReactClientValue→ 目标是"可被客户端还原的模型"
所以,虽然都叫 renderToPipeableStream,它们是两条完全不同的渲染管线。
3) Flight 的最小协议单位:Row(行)
3.1 Row header:id.toString(16) + ':' + tag
在 ReactFlightServer.js 里有一个非常"协议味"的小函数:
js
function serializeRowHeader(tag: string, id: number) {
return id.toString(16) + ':' + tag;
}
也就是说,每一行(row)都至少包含:
- 一个 hex 编码的 id (
toString(16)) - 一个 tag(表示这行是什么类型)
- 以及后续的 JSON/文本内容
例如:
- import 行:
serializeRowHeader('I', id) + json + '\n' - model 行:
serializeRowHeader('D', id) + json + '\n' - error 行:
serializeRowHeader('E', id) + stringify(errorInfo) + '\n'
你可以把 Flight stream 想成"按行发送的一组增量补丁":
- 先告诉你"某个 id 对应一个模块引用"(I)
- 再告诉你"某个 id 对应一段模型 JSON"(D)
- 过程中如果出错,就发错误行(E)
3.2 Flight 的 flush:flushCompletedChunks 就是把不同队列按优先级写出去
还是在 ReactFlightServer.js,flushCompletedChunks(request) 会按队列顺序写出:
- imports(模块)
- hints(资源提示)
- debug(DEV)
- regular model chunks
- error chunks
写出的关键逻辑仍然是:
js
const keepWriting: boolean = writeChunkAndReturn(destination, chunk);
if (!keepWriting) {
request.destination = null;
...
return;
}
这和 Fizz 的"通过 request.destination = null 停止 flowing"是一致的:
- 下游没容量(backpressure) → 立刻停止,等待
drain再继续
因此,虽然 RSC 的内容不是 HTML,但"流式与回压"的工程模型和 Fizz 是同一类。
4) RSC 为什么必须引入"引用类型":ClientReference / ServerReference
RSC 的一个核心约束是:
- Server Components 可以 import Client Components
- 但不能"执行它们",也不能"点属性拿内部导出"
因为 Client Components 的代码最终要在浏览器跑;在服务端,你只能拿到一个"可序列化的引用"。
4.1 引用的形状:$$typeof + $$id
文件:packages/react-server-dom-webpack/src/ReactFlightWebpackReferences.js
它定义了两个 tag:
Symbol.for('react.client.reference')Symbol.for('react.server.reference')
并通过 registerClientReference / registerServerReference 把这些元数据挂到函数/代理对象上:
- client reference:
$$typeof、$$id、$$async - server reference(Server Action):
$$typeof、$$id、$$bound(以及 DEV 下的$$location)
4.2 为什么 Server 端不允许"dot into a client module"?
同一个文件里有一个非常强硬的 Proxy handler:
- 访问
then→ 直接抛错:你不能在 server component 里 await 一个 client module - 访问任意属性 → 直接抛错:你不能对 client module 做点访问
错误文案把约束说得很清楚:
You cannot dot into a client module from a server component. You can only pass the imported name through.
这就是 RSC 的"编程模型边界"落到运行时代码的样子:
- Server 端拿到的不是模块本体,而是一张"可被序列化的能力卡片"。
5) Server Actions:从"函数调用"变成"可序列化的服务端能力"
很多人以为 Server Actions 是框架层(Next.js 等)发明的,其实 React 仓库里已经有一套非常明确的协议实现。
5.1 Client 侧:Server Action 以 ServerReferenceId 形式出现在流里
在 react-client/src/ReactFlightReplyClient.js 里:
serializeServerReferenceID(id)→'$h' + id.toString(16)
也就是说,Server Action 在协议里会以类似 $h1a 这种形式出现(h 只是一个标记前缀)。
5.2 Server 侧:decodeAction 从 FormData 中提取 $ACTION_* 字段
文件:packages/react-server/src/ReactFlightActionServer.js
它做的事可以压缩成一句话:
- 从
FormData里找$ACTION_REF_*或$ACTION_ID_* - 把"引用元数据"解码出来(包含
id与bound args) - 用
serverManifest把idresolve 成真正的函数实现 - 最后把剩余的
formData绑定到 action 的第一个参数上返回
关键逻辑(省略细节):
js
body.forEach((value, key) => {
if (!key.startsWith('$ACTION_')) {
formData.append(key, value);
return;
}
if (key.startsWith('$ACTION_REF_')) {
const formFieldPrefix = '$ACTION_' + key.slice(12) + ':';
const metaData = decodeBoundActionMetaData(body, serverManifest, formFieldPrefix);
action = loadServerReference(serverManifest, metaData.id, metaData.bound);
return;
}
if (key.startsWith('$ACTION_ID_')) {
const id = key.slice(11);
action = loadServerReference(serverManifest, id, null);
return;
}
});
这段代码的意义是:
- Server Action 不是"把函数名拼进 URL"
- 它更像"把一个可验证的引用 id + 绑定参数"编码进一次提交中
而 serverManifest 则是 bundler/框架提供的"引用映射表"。
5.3 你会在这看到一个关键设计:Server 也会"作为 Client"去解析自己
看 ReactFlightActionServer.js 的 import:
- 它从
react-client/src/ReactFlightClientConfig里 importresolveServerReference/preloadModule/requireModule
这也是为什么仓库要把 Flight client runtime(以及 config)拆出来:
- 有些场景下,server 端需要"消费"一段 Flight 编码的数据(比如 bound args)
6) RSC 与 Fizz/Hydration 的契约边界:它们是两套引擎,但会在框架层组合
现在我们可以把三者的职责拆清楚:
- Fizz(SSR):输出 HTML + hydration 指令,目标是"可首屏展示、可续接"
- Hydration:客户端把现有 DOM 与 Fiber 对齐,选择性补水/事件回放属于这条链路
- Flight(RSC):输出"组件模型协议",目标是"把 Server Components 的结果以协议形式交给 client"
在 React 仓库里:
- Fizz 的核心是
ReactFizzServer.js:Task/Segment/Boundary、flush 队列、preamble/postamble、writeStartPendingSuspenseBoundary等 - Flight 的核心是
ReactFlightServer.js:Request/Task、按 row 写出、import/model/error 等 tag
它们的共同点是工程模型:
- 都把"work(推进计算)"和"flush(写出)"解耦
- 都通过
writeChunkAndReturn/destination开关实现 backpressure
不同点是协议与目标:
- Fizz 的消费方是浏览器 DOM
- Flight 的消费方是 Flight client runtime(它最终可能再驱动 React 渲染)
你在真实框架中看到的"RSC + SSR"通常是:
- 首屏用 Fizz 输出 HTML shell
- 同时把 Flight payload 作为一条并行数据流注入/传输
- 客户端用 Flight runtime 消费 payload,然后把 Client Components 部分交给浏览器执行
React 仓库把它们拆开,是为了保持底层引擎的可复用性:
- 协议(Flight)不绑定 DOM
- DOM 的 bundler 绑定(webpack)也不污染协议本体
总结:react-server 不是"SSR 版 react-dom",而是 Flight(RSC)的协议运行时
把本文压缩成三句话:
react-server/react-client实现的是 Flight:一种可流式传输的组件模型协议,它有自己的 Request/Task/flush 体系。react-server-dom-webpack是协议绑定层:解决 bundler manifest、client/server reference 形态、以及 Node/WebStreams 等宿主入口。- RSC 与 Fizz 是两套引擎:它们在框架层通常会组合,但在仓库里边界清清楚楚。
下一篇预告
第 14 篇我们会更进一步:把 Hook 的实现视角接回 Fiber Update Queue,解释"Hook API 的薄、Dispatcher 的厚,以及 lane/调度如何穿过 Hook 更新"。