知

模块四 · 性能、调度与架构

第 15 / 18 节18 分钟阅读型

Fiber 与并发:让出主线程的权利

Fiber 把渲染拆成可中断的工作单元,Lane 决定谁先执行。这一节讲清 Fiber 结构、双缓冲、优先级,以及 transition 与 Suspense 的适用边界。

读完这一节,你能做到

  1. 说清 Fiber 是什么,以及它解决了什么问题。
  2. 用双缓冲解释"可中断渲染"为什么不会撕裂界面。
  3. 说出 Lane 优先级模型的作用。
  4. 判断什么时候该用 useTransition 与 useDeferredValue。

一、Fiber 是什么

Fiber 是 React 的"工作单元",把一次大的渲染拆成许多小任务:

Fiber 节点 = 组件实例 + 工作状态 + 链表指针(child/sibling/return)

它带来三个能力:

  • 可中断:渲染到一半可以让出主线程,先处理用户输入。
  • 可恢复:下次从上次的节点继续。
  • 可丢弃:优先级更高的更新到来时,丢弃低优先级的工作。

在没有 Fiber 之前,递归渲染一旦开始就无法中断,长任务会阻塞输入。

二、双缓冲与一致视图

React 同时维护 current 与 workInProgress 两棵树:

current(屏幕上的) ←→ workInProgress(构建中的)

render 在 workInProgress 上进行,commit 时整体切换。这样即使中途被打断或丢弃,屏幕上的树仍然完整,不会出现"改了一半"的界面。

三、Lane 优先级

React 用 Lane(二进制位)表示更新优先级:

优先级典型来源
同步/紧急点击、输入
默认普通 setState
TransitionstartTransition
空闲预渲染、离屏内容

高优先级更新可以打断低优先级的 render;低优先级任务会在空闲时重做。这就是"输入不卡"的来源:用户打字产生的更新优先于大列表重算。

四、并发 API 的用途

API作用适用
useTransition把更新标记为非紧急搜索过滤、切换大列表
useDeferredValue延迟某个值的更新输入框联动重组件
Suspense声明加载边界代码分割、数据加载

示例:

const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");

function onChange(e) {
  setQuery(e.target.value);
  startTransition(() => setFiltered(filter(e.target.value)));
}

输入框保持响应,列表更新可以稍后完成。isPending 用来显示"正在更新"的提示。

五、并发不是"多线程"

React 的并发仍然是单线程,只是把任务切成片,在浏览器空闲时执行。它不会让计算变快,只会让高优先级任务先执行。

所以:

  • 重计算放进 useMemo 或 Worker,而不是指望 transition。
  • transition 适合"更新频率高、可以稍后"的场景。
  • 不要用它包裹请求,它只管渲染优先级。

六、常见坑

  1. 以为并发渲染会开新线程。
  2. 用 transition 包裹受控输入,导致输入框反而变慢。
  3. 在 Suspense 边界外发请求,加载态无法被捕获。
  4. 忘记处理 isPending,用户以为点击没反应。
  5. 把长任务放进 render,transition 也救不了。

七、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个 5,000 行列表的搜索,分别用普通更新与 useTransition 对比输入延迟。
  2. 用 useDeferredValue 延迟重组件渲染,观察输入流畅度。
  3. 用 Suspense 包裹一个懒加载组件,处理加载与错误态。

② 让 AI 写同一题

把第 1 题交给 AI,补一句 输入框必须保持即时响应。

③ 对比要盯的点

  • 它把输入框自己的 setState 也包进 startTransition 了吗?包了输入就会卡。
  • 耗时的过滤计算它是放在 transition 内还是外?放在外面,transition 白做。
  • 它有没有说“并发渲染更快”?并发只改执行顺序,不改总时长。

④ 一句话取舍

它给的 useTransition,在你机器上真让输入变顺了吗?用 Performance 面板看一眼,不要只看它说“应该会好很多”。

八、自测

  1. Fiber 解决了什么问题?
  2. 双缓冲如何保证视图一致?
  3. Lane 的作用是什么?
  4. transition 与 defer 的区别是什么?
  5. 并发渲染会让计算变快吗?

参考答案要点:可中断渲染;workInProgress 构建后切换;优先级调度;一个标记更新、一个延迟值;不会,只改变执行顺序。

附:Fiber 的遍历顺序

beginWork(向下)→ completeWork(向上)

从根开始,先处理 child,没有 child 就处理 sibling,都没有就 return 到父节点。这个顺序让 React 可以在任意节点暂停与恢复。

commit 阶段则按“先子后父”的顺序执行副作用,保证子组件的 DOM 先于父组件被处理。

附:Lane 的位运算

Lane 用一个整数的不同二进制位表示不同优先级:

0b0001  SyncLane
0b0010  InputContinuousLane
0b0100  DefaultLane
0b1000  TransitionLane

判断“是否包含某个优先级”用按位与,合并用按位或。好处是一次运算就能处理多个优先级,比数组遍历快。

附:并发下的副作用

并发渲染可能让 render 执行多次、被丢弃,所以:

  • 不要在 render 里写 ref、发请求、改全局。
  • effect 在 commit 后执行,不会被“丢弃的 render”触发。
  • useLayoutEffect 仍然是同步的,不要在里做重计算。

并发不是“让代码跑得更快”,而是“让重要的更新先跑”。理解这一点,就不会把 transition 当成性能银弹。

附:时间切片怎么切

React 在 render 阶段每处理完一个 Fiber,就检查是否用完了当前时间片(默认 5ms 左右)。如果用完,就把控制权交还浏览器,让绘制与输入先执行,之后再从暂停的节点继续。

切片单位是 Fiber,不是组件:一个大组件内部有很多节点,也能被切开。这也是为什么 render 必须是纯的——它可能被切成很多段执行。

附:Suspense 的边界

Suspense 捕获的是“子组件抛出的 Promise”,不是所有异步操作。数据请求要配合支持 Suspense 的请求库或框架 loader,普通 useEffect 请求不会被捕获。

边界要放在“加载态有意义”的位置:整页骨架、区块骨架,而不是每个按钮。边界太细,加载态会闪烁;边界太粗,一个慢请求会拖住整页。

附:错误边界与 Suspense 的分工

错误边界捕获渲染错误,Suspense 处理加载态;两者都不能捕获事件回调与异步代码里的错误。事件里的错误要自己 try/catch,异步错误要交给请求库或全局监听。

附:Fiber 与栈调和的差异

栈调和是递归:一旦开始就无法暂停,长任务会阻塞输入。Fiber 改成链表遍历:每个节点是一个工作单元,可以暂停、恢复、丢弃。代价是更多的指针操作与内存占用。Fiber 是调度单位,双缓冲保证一致,Lane 决定先后;并发不是并行,切片让输入优先。理解这三件事,就理解了并发——它改变顺序,不改变总量。先修长任务,再谈优先级;render 可丢,commit 不可丢。切片是手段,一致是底线;优先级服务于体验。理解调度,才理解 React;并发是体验工程,不是性能魔法。先量后调,别把并发当银弹。调度服务于体验,一致先于速度。先一致,再快。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解