人人都会AI编程

5.5 并发渲染的底层原理

更新时间:2026-07-11

并发渲染(Concurrent Rendering)是 React 18 最重要的底层变革,它彻底改变了 React 的更新处理方式。不再是一次状态更新必须同步完成渲染,而是允许渲染过程可中断、可分片、可插队,让应用在高负载下仍能保持流畅的交互体验。

从同步到并发:一个宏观对比

在传统的同步模式(Legacy Mode)中,当状态更新发生时,React 会从根节点开始,一口气完成整棵组件树的渲染,期间无法响应用户的任何输入。如果组件树很深,这次同步渲染可能耗时几十甚至上百毫秒,导致页面掉帧、按钮点击无响应。

并发模式(Concurrent Mode,通过 createRoot 启用)则完全不同:它将一次大的渲染任务拆分成多个小的工作单元,在浏览器空闲时逐步执行。执行过程中,如果有更高优先级的更新(如用户输入)到来,React 可以暂停当前的渲染,优先处理高优先级更新,完成后再恢复之前的工作。这种机制被称为时间切片

// React 17 的同步入口
ReactDOM.render(<App />, document.getElementById('root'));

// React 18 的并发入口
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

仅这一行变更,就打开了并发渲染的大门。

并发渲染的两大基石:Fiber 架构与 Scheduler

并发渲染并非凭空产生,它建立在两套底层系统之上:

  • Fiber 架构:将虚拟 DOM 的每一个节点抽象为一个 Fiber 节点,Fiber 节点形成了可遍历的链表结构。这使得渲染过程不再是递归调用,而是一个可以停顿、可以恢复的循环(workLoop)。每个 Fiber 节点都携带了自身的状态、待处理的更新、以及指向父/子/兄弟节点的指针,这让“中断并恢复”成为可能。
  • Scheduler 调度系统:负责为不同的更新任务分配优先级,并与浏览器的空闲时间进行协调。它基于 MessageChannel 实现空余时间检测,并对外暴露 shouldYield 方法,让渲染循环可以在每一帧的剩余时间不足时主动暂停,交还主线程控制权。

这两套系统协同工作:Fiber 提供了可中断的数据结构,而 Scheduler 决定了何时中断、何时继续

优先级与 Lane 模型

并发渲染的核心挑战是:如何在多个同时发生的更新中,选出最重要的先执行?

React 使用 Lane 模型来表示更新优先级。每个更新都会被分配一个 Lane(赛道),Lane 以二进制位掩码的形式存在,不同的 Lane 代表不同的优先级。

常见的优先级分类(从高到低):

| 优先级 | Lane 名称 | 典型场景 |
|--------|-----------|----------|
| 同步优先级 | SyncLane | 用户输入、点击事件、离散事件 |
| 连续事件 | InputContinuousLane | 拖拽、滚动 |
| 默认优先级 | DefaultLane | 数据请求后的渲染 |
| 过渡优先级 | TransitionLane | 由 useTransition / startTransition 触发的更新 |
| 空闲优先级 | IdleLane | 离屏渲染、分析上报 |

当多个更新同时存在于组件上时,React 会选出优先级最高的 Lane 优先执行,低优先级的 Lane 会被保留,稍后再处理。这实现了“高优先级任务插队”的基础。

// 使用 useTransition 将某个状态更新标记为过渡(低优先级)
function App() {
  const [isPending, startTransition] = useTransition();
  const [query, setQuery] = useState('');
  const [result, setResult] = useState([]);

  const handleChange = (e) => {
    // 高优先级更新:输入框必须立即响应
    setQuery(e.target.value);

    // 低优先级更新:搜索结果可以慢慢显示
    startTransition(() => {
      setResult(search(e.target.value));
    });
  };

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <Spinner />}
      <ResultList data={result} />
    </div>
  );
}

当用户快速输入时,setResult 触发的渲染会因为优先级较低而被中断,React 优先确保输入框的流畅性。一旦用户停止输入,再继续完成搜索结果的渲染。

渲染中断与恢复的完整流程

一次并发更新的执行过程可以概括为以下步骤:

  1. 触发更新:状态变更后,React 创建一个 Update 对象,并标记对应的 Lane,挂载到对应 Fiber 节点的更新队列中。
  1. 入口调度:React 调用 scheduleUpdateOnFiber,根据更新的 Lane 决定是立即同步执行还是调度一个并发任务。对于并发任务,会由 Scheduler 全局管理。
  1. Render 阶段(可中断):从根节点开始,React 进入 workLoop 循环,深度优先遍历 Fiber 树,为每个 Fiber 节点执行协调(beginWork)和收集副作用(completeWork)。在这个过程中,每处理完一个 Fiber 节点,都会通过 Scheduler 的 shouldYield 函数检查是否需要让出主线程:
  • 如果当前帧剩余时间充足,继续处理下一个 Fiber。
  • 如果剩余时间不足(如仅剩 1ms),则暂停遍历,保存当前的 Fiber 指针(即下一个待处理的 Fiber 节点),然后退出循环,让浏览器处理用户事件或绘制。
  • 待浏览器再次空闲时,从上一个保存的 Fiber 指针处恢复遍历,继续后面的工作。
  1. Commit 阶段(同步不可中断):当整棵树的 Render 阶段完成,React 会进入 Commit 阶段,将计算结果一次性地应用到真实 DOM 上。这个阶段很短,而且是同步的,确保 UI 一致性。

这一流程的精妙之处在于:中断只发生在不同 Fiber 节点的处理之间,不会在一个 Fiber 的执行中间插入。每个 Fiber 的 beginWorkcompleteWork 是原子的,确保了数据结构的完整性。

低优先级更新的“丢弃”与“重做”

并发渲染还有一个关键行为:当低优先级更新正在进行时,如果一个更高优先级的更新被触发,React 不仅会中断当前工作,还会完全丢弃已经为低优先级更新生成的部分工作(即 workInProgress 树),然后重新从根节点开始处理高优先级更新。待高优先级更新完成后,再重新开始低优先级更新的渲染。

这避免了低优先级工作成果与高优先级更新的冲突,保证了状态的一致性。虽然看起来有些浪费,但时间切片的粒度足够小(通常每个切片只有几毫秒),丢弃的成本并不高。

时间切片的底层实现

Scheduler 实现时间切片的原理依赖于浏览器的帧率(通常 60fps,每帧约 16.6ms)。浏览器每一帧需要完成样式的计算、布局、绘制以及执行 JavaScript。React 的渲染属于 JavaScript 执行范畴,因此必须在一帧内留给渲染大约 5ms 的可执行时间(留给其他任务的开销),否则会造成掉帧。

Scheduler 使用 MessageChannel 来模拟宏任务,在每个任务单元之间让出主线程:

  1. 当需要执行并发任务时,Scheduler 会将任务放入一个队列,并通过 MessageChannel 发送消息。
  2. 浏览器在事件循环的宏任务阶段执行该消息回调,调用 React 的 workLoop
  3. workLoop 每执行一个时间切片(默认 5ms),就调用 shouldYield 判断是否需要暂停。暂停的方式是 returnworkLoop,让宏任务回调自然结束,主线程归还给浏览器。
  4. 下次浏览器空闲时(由 MessageChannel 再次触发),继续处理后续任务。

这种“合作式调度”确保了并发渲染不会长时间霸占主线程,用户的操作始终能得到及时响应。

并发渲染的实际感知

对于开发者而言,并发渲染主要体现在几个新的 API 和行为:

  • 自动批处理setState 在异步事件(setTimeoutPromise)中也会被自动批量处理,减少不必要的渲染次数。
  • Suspense 的流式加载:组件可以“等待”数据加载完成,渲染过程不会因为单个组件的阻塞而停滞。
  • useTransition / useDeferredValue:让你手动将某些更新标记为低优先级,优化用户输入体验。

理解并发渲染的底层原理,能帮助你更合理地使用这些 API,在复杂的交互场景中做出恰当的优先级划分,让应用始终维持丝滑的用户体验。