Skip to content

第九章:Concurrent React,从一种模式退回一组能力

Fiber 在 2017 年进入稳定版,并不等于应用已经获得并发渲染。内核可以暂停工作,但用户 API、生态库、外部 store、SSR 与 hydration 都必须适应“render 可能重做、多个 UI 版本可能同时准备”的语义。

这段产品化经历了几次名称变化:Async Rendering、Concurrent Mode,最后在 React Conf 2021 被重新表述为“没有 concurrent mode,只有 concurrent features”。2022 年 React 18 发布时,并发 renderer 由新 root API 启用,但只有在使用 transition、Suspense 等能力时,用户才明显感受到新行为。

命名收缩不是放弃目标,而是承认一个全局模式开关会把迁移成本集中爆发。更可行的路线,是让 framework 和应用按 feature 逐步进入。

Interruptible Render 与 Atomic Commit

Concurrent React 的基本执行模型是:render 阶段可以暂停、继续、重启或丢弃,commit 阶段仍一次性落实可见变化。

假设用户输入搜索词,同时列表过滤很昂贵:

  • 输入框更新属于 urgent work,应尽快 commit。
  • 列表结果可以作为 transition,在后台准备。
  • 若用户继续输入,旧列表 render 可以被废弃,只计算最新查询。
  • 屏幕在新列表完成前继续显示旧列表,而不是展示半棵新树。

这不是让 React 更早完成全部工作,而是允许它选择“先完成什么”。响应性来自优先级与可丢弃中间结果。

Lanes:把优先级表示为集合

React 18 的 ReactFiberLane.new.js 用 31 位掩码表示 lane。同步、连续输入、默认、transition、retry、selective hydration、idle、offscreen 等工作占据不同 bit 区间。

root 保存 pending、suspended、pinged、expired 等 lane 集合。getNextLanes 先找未被 suspension 阻塞的高优先级工作;若某批工作在等待 promise,React 可以转去处理其他 lane;promise resolve 后相应 lane 被 ping,再次进入候选。

位掩码让集合运算快速,也允许一次处理多个相同优先级 lane。Transition lane 轮换分配,相关更新还可 entangle。相比 expiration time,它更能表示“多批工作并存但不应混为一个截止时间”。

代价是,更新队列必须在一次 render 中跳过不属于当前 lane 的 update,并保留足够 base state 供未来重放。所谓 concurrent state 并不是维护多个任意分支,而是在严格队列规则下为不同优先级构造一致快照。

startTransition:由应用标注“可以等”

React 无法仅凭 setter 内容判断更新是否紧急。startTransition 让应用声明一组 state update 是 non-urgent:

js
setInputValue(next); // urgent
startTransition(() => {
  setSearchQuery(next); // transition
});

React 会优先保证输入响应,transition 可被中断。useTransition 还返回 pending 状态,应用可决定是否显示进度提示。

这里没有把异步函数自动放进线程;标记只影响 React update 的 lane。昂贵同步代码若在事件回调中、React 外部或单个不可拆 render 内运行,仍会阻塞。

useDeferredValue 则让某个值的“延迟版本”在低优先级追赶,适合无法直接控制上游 setter 的场景。两者都把调度意图纳入组件 API,而不是让 scheduler 猜测。

Suspense:等待成为树结构的一部分

Suspense 最初随 React 16.6 用于 code splitting:lazy 组件未加载时,最近的 <Suspense fallback> 显示占位。其内部模型是组件 render 遇到未完成 thenable 时“suspend”,reconciler 捕获它并标记边界;thenable 完成后安排 retry。

这种机制后来扩展到数据与 server rendering。Suspense 的关键不是 promise 语法,而是把“这棵子树现在不能完成”变成 reconciler 能处理的控制信号。边界定义了可独立等待与 reveal 的 UI 单位。

如果没有 concurrent renderer,Suspense 很容易让已经显示的内容突然退回 fallback。Transition 允许 React 保持旧 UI,直到新树与数据一起准备好。调度和等待因此必须一起设计。

React 18:渐进启用的并发基础

React 18 的发布说明列出 automatic batching、transitions、Suspense 改进和新的 streaming server renderer。使用 createRoot 代替旧 ReactDOM.render,root 才进入新行为与 API。

这条迁移边界让应用可以逐 root 升级,也让库作者有时间适配。React 18 Working Group 在公开讨论中收集 framework、library 和 application 的问题,说明并发语义不能只由 core 团队在内部完成。

Automatic batching 扩展到 promise、timeout 和原生事件等更多来源,减少多次 state update 的重复 render。它是并发基础设施带来的行为变化,即使应用没有显式使用 transition 也能受益。

External Store 与 Tearing

React 内部 state 可以按 lane 保存和重放,外部 store 却可能在一次 concurrent render 中间改变。若树的上半部读旧值、下半部读新值,就出现 tearing。

useSyncExternalStore 为 store 订阅定义正式协议:React 读取 snapshot,并在提交前确认它没有变化;server rendering 还可提供 server snapshot。名称中的 Sync 表示 store mutation 需要以能避免 tearing 的同步方式被观察,而不是所有 React render 都退回同步。

这个 API 体现 core 与生态边界的重新谈判。早期 React 可以说“状态库在外部”;并发后,外部库若不遵守 snapshot 契约,会破坏 React 对一棵树一致性的保证,因此核心必须提供集成原语。

Fizz:Streaming SSR 与 Selective Hydration

旧 SSR 往往经历瀑布:服务器等全部数据,生成完整 HTML;浏览器下载全部 JS;整棵树 hydrate 后才能交互。React 18 的 Fizz 以 Suspense boundary 分割 server work:先发送 shell 和 fallback,后续内容完成时流式补入。

客户端也不必严格从根到叶一次性 hydrate。用户与尚未 hydration 的区域交互时,React 可以提高相关 boundary 的 hydration 优先级;其他区域继续延后。这把 network arrival、code loading、user input 与 scheduler 连接起来。

Fizz 仍然输出 HTML,目标是让同一 client component tree 更早可见、分段可交互。下一章的 Flight/RSC 输出不同协议,解决的是客户端 bundle 与服务器能力边界,不能把二者混为一谈。

Strict Mode 为什么会“故意多执行”

为了暴露不安全副作用,React 18 Strict Mode 在开发环境会模拟额外 mount/unmount、重复调用部分 render 和 effect setup。生产环境不会照搬这些调试行为,但它迫使应用验证 effect 清理和 render 纯度。

不少迁移痛苦来自代码过去依赖“mount 只发生一次”或“render 调一次就一定 commit”。Concurrent React 并没有凭空破坏可靠语义,而是把原本未写入契约的时序假设变成可观察问题。

为什么放弃“Concurrent Mode”叙事

把并发称为 Mode,容易让人以为:

  • 开启后所有渲染自动更快。
  • 整个应用必须一次迁移。
  • 旧模式与新模式是两套完全分开的 React。
  • 并发等于多线程。

最终 feature-based 叙事更精确:transition 解决可延迟更新,Suspense 解决等待边界,streaming SSR 解决分段交付,selective hydration 解决交互优先级,external store API 解决一致 snapshot。它们共享 Fiber/Lanes 基础,却可以由 framework 和应用逐步采用。

本文判断:React 从“模式”退回“能力”,是一项成熟的产品化决策。Fiber 提供通用机制,但用户应为具体体验问题选择 feature,而不是为抽象架构切换全局开关。

Concurrent React 重新组织了浏览器中的时间;Server Components 会重新组织执行地点。下一章讨论组件的一部分如何只在服务器运行,另一部分留在客户端,以及 Flight 为什么需要 bundler 与 framework 成为协议参与者。

Engineering history, reconstructed from primary sources.