Suspense 和 Concurrent Rendering
1. 版本与定位
| 能力 | 首次对用户可见 | 说明 |
|---|---|---|
<Suspense> + React.lazy | React 16.6 | 客户端代码分割、加载占位 |
| Concurrent Rendering(可中断 render) | React 18 + createRoot | 依赖 Fiber + Scheduler 时间切片 |
| Suspense 数据流 / 流式 SSR 增强 | React 18 | 服务端可先出 fallback,再流式替换 |
use 读 Promise | React 19 | 与 Suspense 配合,见 04-Hooks §10 |
并发渲染不是多线程,而是同一条主线程上可打断、可恢复、可优先的 Reconciler 工作方式;Suspense 是「子树还没准备好时,边界内先显示 fallback」的协调协议。二者常一起出现,但概念上可分开理解。
2. Concurrent Rendering 是什么
2.1 心智模型
- 同步模式(Legacy
ReactDOM.render):一次更新从 render 到 commit 尽量跑完,长任务会堵住点击、滚动。 - 并发模式(
createRoot+ Concurrent Features):Render 阶段可拆成多段,由 Scheduler 在段与段之间shouldYield,高优 lane(如点击)可插队;Commit 仍一次性、不可打断。
flowchart LR
subgraph sync [同步模式 17-]
A1[更新] --> B1[整棵 render]
B1 --> C1[commit]
end
subgraph conc [并发模式 18+]
A2[更新] --> B2[render 片段]
B2 --> Y{yield?}
Y -->|是| I[处理输入 / 更高优更新]
I --> B2
Y -->|否| C2[commit]
end2.2 用户能感知到的收益
| 机制 | 体验 |
|---|---|
| 时间切片 | 大列表重渲染时输入仍跟手 |
| Lane 优先级 | 点击比 startTransition 里的筛选更快上屏 |
| 可丢弃的 render | 过时的 Transition 结果可被更新的 state 作废,减少「闪一下旧 UI」 |
useDeferredValue | 输入框即时、重内容稍后跟上 |
2.3 实现上靠谁(不贴长源码)
workLoopConcurrent:workInProgress存在时循环performUnitOfWork,直到shouldYield()(内部即 Scheduler 的shouldYieldToHost)。- continuation:一轮没遍历完,Scheduler 下次接着跑(见 05-Scheduler §3.3)。
getNextLanes:从 root.pendingLanes 选出本轮要处理的最高优车道(五档见 02-Fiber §4.1)。
flushSync 仍走同步路径,故意绕过切片,用于必须与 DOM 立即一致的少数场景。
3. Suspense 是什么
3.1 职责边界
<Suspense fallback={...}> 只负责一件事:后代在「尚未就绪」时,由这一层决定显示 fallback 还是正式子树。常见「未就绪」来源:
| 场景 | 典型 API | 版本 |
|---|---|---|
| 懒加载组件 | React.lazy + import() | 16.6+ |
| 异步数据(客户端) | use(promise) | 19+ |
| 服务端异步 RSC | Server Component await | 18+ 流式 / 19 稳定 |
| 资源加载(样式等) | 框架集成 | 18+ |
3.2 基本用法
<Suspense fallback={<Loading />}>
<AsyncTree />
</Suspense>可嵌套:内层边界只兜底内层子树,外层 fallback 不会替代已就绪的内层内容。
3.3 与 Error Boundary 的分工
| Suspense | Error Boundary | |
|---|---|---|
| 触发 | 子树 suspend(抛出 thenable / 特殊异常) | 子树 throw 普通 Error |
| UI | fallback 加载态 | fallback 错误态 |
| 恢复 | 数据/模块就绪后自动重试 render | 需用户操作或 state 重置 |
4. Suspense 实现原理(总结)
4.1 「挂起」不是 return,而是 throw
子组件在 render 中若发现 Promise 仍为 pending(use / 部分数据层),Reconciler 会抛出带 thenable 的 SuspenseException(概念名,见 ReactFiberThenable.js),并带上 retryLane(常落入 RetryLanes,见 Fiber 文档辅助车道)。
这不是业务错误,而是控制流:向上冒泡直到最近的 Suspense 边界捕获。
4.2 边界内发生什么
updateSuspenseComponent(ReactFiberBeginWork.js)根据是否应显示 fallback:
- 显示 fallback:挂载 fallback 子树,给 Fiber 打上
DidCapture等标记,正式子树暂缓或隐藏。 - 显示 primary:子树已就绪,渲染
children,隐藏 fallback。
Fiber 的 memoizedState(SuspenseState) 会记录是否处于「已超时显示 fallback」、retryLane、注水相关状态等,供下次更新与 SSR 对齐。
4.3 就绪后如何恢复
- Promise resolve 后,在对应 retryLane 上调度一次更新。
- 再次 render 时走 primary 路径;若仍 suspend,继续 fallback。
- React 18+:服务端可对未就绪片段先流式输出 fallback HTML,再在数据就绪后追加 chunk(与 07-ReactServerComponents 衔接)。
sequenceDiagram
participant C as 子组件 render
participant R as Reconciler
participant S as Suspense 边界
participant Sch as Scheduler
C->>R: Promise pending
R->>R: throw SuspenseException
R->>S: 捕获,渲染 fallback
Note over C: Promise resolve
Sch->>R: retryLane 调度更新
R->>S: 渲染 primary children4.4 多个边界与「作用范围」
- 只影响子孙;兄弟组件不因旁支 suspend 而卸载。
- 嵌套时最近的边界先捕获;外层可继续显示自己的 fallback。
- 列表里每项包一层 Suspense,可实现逐条就绪,避免整页一个 loading。
5. use 与 Suspense(React 19)
use(promise) 在 pending 时走与上节相同的挂起路径;fulfilled 返回值,rejected 抛错(由 Error Boundary 处理)。可在条件分支中调用,但不替代「数据应在哪一层 fetch」的架构选择。
与 useEffect + setState 相比:use 把异步纳入 render 协调,与 Suspense fallback 一体,而不是先空渲染再 effect 补数据。
详见 04-Hooks §10.3。
6. Transition 与延迟值(React 18)
6.1 useTransition / startTransition
作用:把回调里触发的更新标为 TransitionLanes(非紧急),避免挡住 Sync / 连续输入。
要点:
- 返回
[isPending, startTransition];pending 表示 Transition 尚未完成。 startTransition可在组件外调用;内部会临时提升/设置更新优先级上下文,执行callback()后恢复。- 与 Actions(19) 结合时,异步 Action 也可在 Transition 中执行(见 04-Hooks §10)。
6.2 useDeferredValue
作用:对外仍用「上一拍」的 deferredValue 渲染重子树,在后台用 Transition 追上最新 value。
与防抖区别:无固定延迟;与 React 调度统一,且可被更高优更新打断。
React 19:可选 initialValue,首屏先用初始值再 deferred 更新。
7. 并发 + Suspense 协作一览
| 用户操作 | Lane / 机制 | UI 表现 |
|---|---|---|
| 输入框打字 | Sync / Default | 即时 |
startTransition 改筛选 | Transition | 可先保持旧列表,新结果就绪后切换 |
| 进入懒加载路由 | Suspense + lazy | 边界内 fallback |
use(fetch) 未返回 | Suspense + use | 同左 |
flushSync | Sync,不 yield | 强制同步 commit,慎用 |
8. 常见误区
- Suspense 不是「加载组件」本身:要有子树真的 suspend;仅
isLoading状态不必包 Suspense(当然可以用,但不是机制要求)。 - fallback 会替换整段子树:布局应用外层边界兜住,避免整页闪烁。
- 并发不等于后台线程:CPU 仍在主线程,靠 yield 改善响应。
- Commit 不会一半提交:不会出现「DOM 改了一半」的并发 commit;未完成 render 不会 partial commit 到页面(未完成树会被丢弃或继续 WIP)。
9. 源码阅读路径
| 主题 | 文件 |
|---|---|
| Suspense 组件 | ReactFiberSuspenseComponent.js |
| Thenable / 挂起异常 | ReactFiberThenable.js |
| beginWork 捕获 | ReactFiberBeginWork.js |
| 并发循环 / yield | ReactFiberWorkLoop.js |
| Lane / retry | ReactFiberLane.js |
10. 总结
- Concurrent Rendering(18+):可中断的 Render,依赖 Scheduler 与 Lanes;Commit 仍原子。
- Suspense:通过 throw + 边界捕获 切换 fallback / primary,用 RetryLanes 在就绪后重试。
use(19) 把 Promise 纳入同一套挂起协议。- Transition / deferred 把「可慢半拍」的更新与紧急更新分离,改善交互。
与 05-Scheduler调度器、07-ReactServerComponents 连读,可覆盖「客户端切片 + 服务端流式 Suspense」全链路。