为什么需要时间切片
在 React 15 及之前的 Stack Reconciler 中,一旦开始渲染,就必须一口气完成整棵组件树的比较和更新。如果组件树非常庞大,这个同步过程可能耗时几十甚至上百毫秒。在浏览器中,渲染任务发生在主线程上,长时间不释放主线程会导致页面暂时无法响应用户的点击、输入或动画,表现为掉帧和卡顿。
想象一个场景:用户正在输入框中快速打字,同时页面有一个很大的列表也在更新。在同步渲染下,每次按键触发的状态更新都要等待整个列表的渲染完成后才能响应下一次输入,体验极差。
时间切片正是为了解决这个问题:它允许 React 将一个大块渲染任务拆成多个小块,在浏览器帧的空闲期间分段执行,并在每一片执行完后主动让出主线程,让浏览器有机会处理更紧急的用户交互和绘制。
它如何工作
时间切片基于 Fiber 架构的两个关键设计:
- Fiber 节点的链表结构:虚拟 DOM 树中的每个节点都是一个 Fiber 对象,除了组件信息外,还包含了
return(父节点)、child(第一个子节点)、sibling(下一个兄弟节点)三个指针,构成一个可遍历的链表。这使得递归的同步遍历变成了可逐步推进的迭代过程。
- 工作循环(Work Loop):React 的 Scheduler 包维护一个任务队列,通过一个循环不断取出最小工作单元(一个 Fiber 节点的处理),执行完毕后检查是否还有剩余时间。如果没有时间了,就中断循环,将控制权交还给浏览器;等浏览器空闲时,再从上次中断的地方继续执行。
这个“是否还有时间”的判断,依赖 Scheduler 的 shouldYield 方法。它的基本原理是利用浏览器提供的 MessageChannel 或 requestAnimationFrame 来确保在每一帧(约 16.6ms)的剩余时间内执行任务,如果时间不足就暂停。
▬▬▬▬▬▬▬▬▬▬ 一帧(16.6ms)▬▬▬▬▬▬▬▬▬▬
█ 用户交互 █ JS执行 █ 渲染绘制 █ 空闲时间
↑ React 时间切片在此执行
在 空闲时间 中,React 会处理几个 Fiber 节点,然后 shouldYield 返回 true,暂停工作,交还控制权。下一帧再继续。
对开发者的实际影响
时间切片对大多数开发者是透明的。不需要显式调用任何 API,只要你使用的是 React 18 的并发模式(createRoot),React 就会自动启用时间切片来渲染更新。但理解它能帮助你解释一些行为,并编写更顺滑的用户界面:
1. 低优先级更新可以被中断
React 18 引入了 useTransition 和 useDeferredValue,这些 API 明确将某些状态更新标记为“低优先级”。这些低优先级更新会通过时间切片来执行,如果用户在此期间触发了高优先级更新(例如输入、点击),React 可以中断正在进行的低优先级渲染,先处理高优先级的,确保交互即时响应。
function App() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
// 高优先级:立即更新输入框的值
setQuery(e.target.value);
// 低优先级:搜索结果列表的更新允许延迟
startTransition(() => {
setSearchResult(filterResults(e.target.value));
});
};
return (
<>
<input value={query} onChange={handleChange} />
{isPending && <p>加载中...</p>}
<SearchResults results={searchResult} />
</>
);
}
输入框的响应永远是最快的,而搜索结果的渲染可能会被中断,从而不影响用户打字流畅度。
2. 更流畅的用户体验,无需手写 debounce
过去为了处理大量输入触发的频繁更新,开发者常手动加入 debounce(防抖)来减少渲染次数。有了时间切片和并发模式,React 自己可以将多次渲染合并或延迟,即使不用 debounce,UI 也不会卡顿。当然,对于网络请求的防抖仍有必要,但 UI 渲染方面可以更省心。
3. 渲染可能不会一次性看到最终结果
因为渲染过程被切片,你在 React DevTools 中观察或以控制台打印时,可能会发现渲染阶段性状态不是“原子性”完成的。例如,在一个低优先级更新中,你可能会短暂看到一棵不完全的组件树(旧树和新树混合)——但这只在开发调试时可感知,用户看到的是最终 Commit 阶段同步更新后的完整界面,不会有视觉闪烁。
注意事项与局限性
- 不要依赖渲染过程的“瞬时完成”:时间切片意味着渲染可能跨越多个浏览器帧,如果有副作用依赖特定渲染时机,请使用
useEffect,它总是在 Commit 阶段之后同步执行。 - 并非所有更新都分片:高优先级更新(如用户直接交互触发的用户阻塞级更新)会尽可能同步渲染,不被分片中断。
- 服务端渲染不支持时间切片:时间切片目前只存在于客户端,服务端渲染仍然是同步的,不过 React 18 的流式 SSR 通过分块传输提供了类似的延迟体验。
时间切片是 React 走向“并发 UI”的关键一步,它使得复杂界面的更新不再占用主线程而打断用户操作,是提升实际用户体验不可或缺的底层能力。