在 React 15 及更早版本中,React 使用 Stack Reconciler(栈协调器) 来处理虚拟 DOM 的对比和更新。它的核心特征是同步、不可中断的递归渲染——一旦开始更新,就必须一次性完成整个组件树的对比和渲染,中间不能暂停,也无法让出主线程。
同步渲染的工作方式
当组件状态发生变化时,Stack Reconciler 会从根组件开始,递归地遍历整个虚拟 DOM 树,逐层对比新旧节点,计算差异,并在同一次调用栈中完成所有真实 DOM 的更新。整个过程是一气呵成的,直到整个组件树更新完毕,浏览器才有机会去处理其他任务(如响应用户输入、执行动画帧、绘制新页面)。
用户点击 → React 开始更新 → 递归遍历整棵组件树 → 完成所有 DOM 操作 → 浏览器重新绘制
↑ ↓
└────────────────── 这段时间内浏览器完全被阻塞,无法响应用户 ───────────────────┘
卡顿问题的根源
这种同步渲染在大型应用中会直接导致页面卡顿甚至无响应。核心原因在于 JavaScript 是单线程的,React 的更新过程如果长时间占用主线程,浏览器就无法执行帧渲染、用户事件处理等关键任务。
举个例子:假设有一个包含大量列表项和复杂嵌套组件的页面,当用户快速输入关键词进行搜索过滤时,每次按键都会触发整个页面的重新渲染计算。如果一次渲染耗时 200ms,那么用户就会明显感觉到输入卡顿,界面跟不上打字速度。
// 一个包含大量子组件的复杂页面
function Dashboard() {
const [keyword, setKeyword] = useState('');
// 每次输入都会触发整个 Dashboard 及其所有子组件重新渲染
return (
<div>
<input value={keyword} onChange={e => setKeyword(e.target.value)} />
<HeavyDataTable filter={keyword} />
<ComplexChart filter={keyword} />
<ActivityLog filter={keyword} />
</div>
);
}
在 Stack Reconciler 下,每一次 setKeyword 都会同步地重新计算 HeavyDataTable、ComplexChart、ActivityLog 这三棵组件树的虚拟 DOM 差异。如果这些组件内部状态复杂、子组件数量多,单次更新就可能远超一帧的时间预算(16ms),导致掉帧和卡顿。
更深层的问题:无法区分更新优先级
Stack Reconciler 的另一个致命缺陷是无法区分高优先级更新和低优先级更新。所有状态变更都被一视同仁地立即处理,无论它们来自:
- 高优先级:用户输入、按钮点击、动画交互
- 低优先级:服务器返回的数据、后台计算结果的展示
这意味着一个本该立刻响应的用户点击,可能被一个正在进行的大列表渲染阻塞,直到整个列表渲染完成才有反应。这种体验在复杂应用中会变得完全不可接受。
实际开发中的典型症状
使用 React 15 开发中大型应用时,你大概率会遭遇以下问题:
- 输入框延迟:在包含大量数据的页面中输入,字符出现肉眼可见的滞后。
- 动画卡顿:CSS 动画或 requestAnimationFrame 驱动的动效在状态更新期间会不流畅。
- 长时间白屏或无响应:路由切换或数据加载时,界面完全“冻住”,无法进行任何操作。
这些问题的共同根源就是 Stack Reconciler 的同步递归更新机制——它强行占用了主线程,剥夺了浏览器响应高优先级任务的能力。
为什么需要新的架构
Stack Reconciler 的痛点暴露了 React 在复杂场景下的性能上限:“全量同步更新”无法适应现代交互对响应速度的要求。为了解决这个问题,React 团队重写了核心协调算法,引入了 Fiber 架构——一种能够将渲染工作拆分为多个小任务,并支持暂停、恢复和优先级调度的可中断渲染引擎。这将是下一节详细展开的内容。
理解 Stack Reconciler 的局限性,是理解 Fiber 设计动机的关键。正是这些实际场景中的卡顿和无响应,推动了 React 从“同步不可中断”向“异步可中断”的架构演进。