第十章:Server Components,组件树跨过网络边界
React 从第一代就支持 server rendering,但“在服务器执行组件”并不自动等于 Server Components。传统 SSR、Streaming SSR、React Server Components 与 Server Functions/Actions 解决的是不同问题:
| 机制 | 主要输出 | 客户端是否下载该组件代码 | 主要目标 |
|---|---|---|---|
| 传统 SSR | 完整 HTML 字符串 | 是,随后 hydration | 首屏内容与 SEO |
| Fizz Streaming SSR | 分段 HTML + hydration 指令 | 是 | 更早发送 shell、分段可交互 |
| React Server Components / Flight | 组件模型、数据与模块引用 | Server Component 本身不下载 | 减少客户端代码、靠近数据源 |
| Server Functions / Actions | 跨边界调用与结果 | 客户端下载引用桩 | 把 mutation、pending、optimistic UI 接入 React |
这些机制可以组合:服务器先用 RSC 生成树,再用 Fizz 把其中的 client component shell 渲染成 HTML;客户端接收 Flight 更新并协调已有树。把它们都称为“SSR”会遮住真正的新边界。
从 HTML 字符串到分段工作
早期 renderToString 把组件树同步变成字符串。React 16 重写 server renderer 并提供 stream,但整棵 React 逻辑仍需大体完成,才能安全组织输出。React 18 的 Fizz 为每个 request 建立 task、segment 与 Suspense boundary:某段等待数据时,shell 可以继续输出;完成后再发送用于替换 fallback 的内容。
这改变了交付顺序,却没有改变代码归属。参与 hydration 的组件 JavaScript 仍要到客户端,客户端仍需构建相应 Fiber tree。
React Server Components 的研究从另一个瓶颈出发:很多组件只是读取数据库、文件系统或服务数据,格式化后组合 UI;把这些组件连同依赖全部发送浏览器,既增加 bundle,也让数据请求容易形成 client-server waterfall。
“Zero Bundle Size”不是整个应用零 JavaScript
2020 年 12 月,React 团队发布《Introducing Zero-Bundle-Size React Server Components》,随后通过 RFC #188 公开设计。
“zero bundle size”指 Server Component 及其仅服务器依赖不进入 client bundle,不是说所有 React 应用无需 JavaScript。需要 state、effect、event handler 或浏览器 API 的组件仍是 Client Component,并发送到客户端。
RSC 的目标包括:
- 在服务器直接访问数据源。
- 避免把大型格式化库、数据库 client 等依赖送到浏览器。
- 让 server/client component 自动参与 code splitting。
- 将数据获取与消费它的组件共置,减少层层 props 和网络 waterfall。
- 在保留客户端 state 的情况下流式更新服务器生成的子树。
use client 定义的是模块图边界
Server Component 默认只在服务器环境执行,不能使用 useState、useEffect 或 DOM API。需要交互的模块以 'use client' 标记入口;从该入口可达的模块进入 client graph。
这条边界不是运行时 if (server)。Bundler/framework 必须分析模块图,为 Client Component 生成引用清单。服务器遇到 Client Component 时,不执行它的浏览器逻辑,而在 Flight 输出中写入 client reference:模块 ID、导出名和 chunk 信息。客户端收到后再加载模块并创建 element。
因此 'use client' 的放置会影响 bundle 边界。把它加在高层模块,可能把大量依赖一起推入客户端;放得过低,又会让 props 序列化与组件拆分更繁琐。
同理,Server Component 传给 Client Component 的 props 必须能通过 Flight 协议表示。函数通常不能直接跨边界,除非它是注册过的 Server Function reference。
Flight 输出的不是 HTML
现代 ReactFlightServer.js 处理 request、task、chunk、thenable、client/server reference、hint 和错误信息。它把组件计算结果编码为增量记录;客户端的 Flight decoder 逐步解析记录,解决引用和 promise,再把模型交给 reconciler。
概念上可以这样理解:
Server Component render
│
├── 普通可序列化数据 ──► Flight chunk
├── Client Component ───► module reference
├── 未完成 thenable ────► pending chunk,完成后续写
└── Server Function ────► server reference
│
▼
Client decoder → React element/model → client reconcilerFlight 是 React 与 framework/bundler 的协议接缝。React core 定义模型和编码逻辑,但模块如何编号、chunk 如何加载、请求怎样路由、缓存与部署如何配置,都需要集成层完成。
RSC 与 Fizz 如何组合
一个常见 framework pipeline 大致分两层:
- RSC render:执行 Server Components,产生 Flight model,其中包含 Client Component references。
- SSR render:用这份 model 进一步生成初始 HTML,尽快给浏览器可见内容。
浏览器先显示 HTML,再下载 client modules 并 hydration;后续导航可以只请求新的 Flight payload,由客户端 reconciler 把服务器产生的新 subtree 合入现有树,保留未被替换的 client state。
因此 RSC 不是“每次请求整页刷新”,也不是传统 REST 返回 JSON 后由客户端模板渲染。服务器返回的是与 React element/模块引用语义一致的结构化流。
use 与 Suspense:读取等待中的值
React 19 引入稳定的 use API,可在 render 中读取 promise 或 context。读取未完成 promise 时组件 suspend,最近 Suspense boundary 决定 fallback 与 reveal。与 await 不同,use 可以在条件中调用,因为它不是依赖 Hook 链表位置保存状态的普通 Hook。
在 Server Component 中,async function 与 await 也可直接使用;在 Client Component 中,framework 可把服务器创建的 promise 传入并通过 use 读取。无论形式如何,thenable 状态都必须与 request/Flight/Suspense 调度对齐。
这让数据等待进入组件控制流,也增加缓存要求。若每次 render 都创建一个全新 promise,React 无法稳定复用;framework 通常需要请求级 memoization、数据缓存或预加载协议。
Actions:Mutation 也进入调度模型
React 19 将 async transition 中的函数称为 Action,并提供 useActionState、useOptimistic、useFormStatus,以及 <form action={fn}> 等 DOM 集成。Action 把 pending、error、optimistic state 和最终 commit 纳入 transition。
当函数通过 'use server' 注册为 Server Function,client 可持有引用并发起调用。Framework 负责把参数编码、发送到服务器、执行函数、返回结果或新的 RSC payload。
这里容易产生两个误解:
- Server Action 不是任意 RPC 自动安全。 它仍需认证、授权、输入验证和 CSRF/部署层保护。
- Action 不等于所有数据请求。 它更适合用户触发的 mutation;读取数据通常由 Server Component、cache 和 framework loader 组织。
React 提供的是 UI 状态与异步操作的编排语义,不会替应用定义业务安全策略。
React 19:用户 API 稳定,集成 API仍可能变化
React 19 在 2024 年 12 月稳定,Actions、use、form integration 和面向用户的 Server Components 能力进入正式版本。但官方明确提醒:供 bundler/framework 实现 RSC 的底层 API 仍可能在 minor version 间改变。
这是一个重要的稳定性分层:应用作者看到的 component model 可以稳定,framework 作者使用的 Flight manifest、server-dom 包和打包条件仍与 React 版本紧密耦合。RSC 因而首先在 Next.js 等深度集成 framework 中普及,而不是像 useState 一样单独安装即可完整使用。
React 19.2 又加入 partial pre-rendering 与 resume API:静态 shell 可提前生成,动态部分在之后请求中恢复。它进一步模糊 build-time、request-time 和 client-time 的边界,也进一步要求 framework 管理 postponed state、缓存和部署生命周期。
新边界带来新的安全后果
当 React 只在浏览器协调 DOM,它主要处理不可信字符串的转义和事件。Server Functions 与 Flight decoder 加入后,React 开始解析来自网络的编码 payload、解析模块/函数引用,并可能触发服务器执行路径。信任边界显著扩大。
2025 年 12 月,React 团队披露 React Server Components 解码路径中的严重远程代码执行漏洞,随后又发布拒绝服务和源码暴露相关更新;2026 年仍有后续 advisory 与补丁版本。详见 React 官方的首份安全公告及后续公告索引。
这里不展开利用细节。对项目史更重要的结论是:RSC 不只是渲染优化,它让 React 的一部分成为服务器协议解析器。安全响应、版本协调和 framework supply chain 从此是核心架构责任。
本文判断:Server Components 把 React 最初的“UI 是数据的函数”推过网络:服务器计算一部分树,客户端继续解释和协调。但每跨一条边界,原本由应用显式处理的模块、缓存、序列化和安全复杂度就会进入 React 与 framework 的共同基础设施。
RSC 展示了 React 如何把更多工作收进运行时与框架。最后一章看同一趋势的另一面:Compiler 把手工 memoization 前移到构建期;CRA 的退场把应用集成上移到 framework;Foundation 则把项目所有权从单一公司移到独立组织。