时间切片原理深度解析
一句话概括
时间切片(Time Slicing)把一个长耗时的渲染任务切成一串 5ms 的小片段,利用浏览器帧空闲逐步执行——这保证了即便页面里有 10000 个元素在渲染,用户的点击和滚动也不会被阻塞。
核心知识点
1. 为什么不能一口气渲染完
浏览器 60fps → 每帧只有 16.6ms。如果 React sync 渲染耗时 100ms,就占用了 6 帧——这段时间用户输入全部无响应,动画掉帧。
时间切片的思路:每个节点处理完就检查”时间够吗”,不够就让出主线程,下一帧再来。
2. Scheduler 的工作循环
React 不用 requestIdleCallback(兼容性差、触发频率低),自己用 MessageChannel 实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// React Scheduler 核心循环(简化)
function workLoop(hasTimeRemaining, initialTime) {
let currentTask = peek(taskQueue);
while (currentTask) {
if (currentTask.expirationTime > currentTime && shouldYield()) {
break; // 时间片用完,暂停
}
const continuation = currentTask.callback();
if (typeof continuation === 'function') {
currentTask.callback = continuation; // 未完成,继续
} else {
pop(taskQueue); // 完成
}
currentTask = peek(taskQueue);
}
if (currentTask) port.postMessage(null); // 下一帧继续
}
MessageChannel 是宏任务,在事件循环中排在微任务之后,让浏览器有机会在两次回调间执行渲染。
3. 5ms 从哪来
1
2
3
60fps 帧预算: 16.67ms
布局 + 绘制: ≈ 10ms
留给 JS: ≈ 5-6ms ← 这就是切片大小
调大切片(如 10ms)→ 渲染完成更快但更卡;调小(2ms)→ 更流畅但完成更慢。这叫”响应性 vs 吞吐量”的权衡。
4. shouldYield 的检查逻辑
1
2
3
4
5
6
7
8
9
function shouldYieldToHost() {
const timeElapsed = performance.now() - startTime;
if (timeElapsed < 5) return false; // 没超预算,继续干
// 检查是否有用户输入等待处理(Chrome 87+)
if (navigator.scheduling?.isInputPending?.()) return true;
return true; // 超预算就让出
}
isInputPending 是近年 Chrome 的新 API,让 JS 能主动问”有人在点我吗”,进一步提升响应速度。
5. 可中断 vs 不可中断的边界
- Render 阶段(beginWork/completeWork):可中断。操作的是 workInProgress 的”草稿”树,中断了丢弃就行。
- Commit 阶段(DOM 操作):不可中断。操作的是真实 DOM,中断会留下半更新 UI,用户看到残次品。
「其实你每天都在用」
无限滚动列表:往下滑加载更多数据,React 在帧空闲时间分批渲染新行,滚动帧率稳定 60fps。
搜索框输入过滤 50000 条数据:
useDeferredValue(query)让输入框立即响应(SyncLane),过滤+渲染走 TransitionLane 分片执行。Tab 切换大数据页面:
startTransition让 tab 按钮高亮立即生效,新 tab 内容后台切片渲染,快速切 tab 不卡。Chrome Performance 面板里的锯齿图:时间切片模式下,Main 线程的火焰图是多个短条(每个 ~5ms),不再是单一长条。
React DevTools Profiler:灰色空白段就是 React 主动让出的空闲时间,用来给浏览器处理你和页面的交互。
常见误解 (FAQ)
❌ 误区 1:”时间切片让渲染变慢了”
总耗时可能略有增加(上下文切换开销),但用户感知流畅度大幅提升。就像你可以在做饭的每个步骤间接电话,虽然总体多花了 3 分钟但没漏接。
❌ 误区 2:”React 用的是 requestIdleCallback”
React Scheduler 用的是 MessageChannel + requestAnimationFrame 的自研方案。rIC 触发频率太低(20-50fps)且在 Safari 上不支持。
❌ 误区 3:”任何 React 应用都需要时间切片”
如果组件树很小(<1000 节点),同步渲染本身就不超过一帧,时间切片的调度开销反而多余。Fiber 的价值在复杂场景中体现。
❌ 误区 4:”useDeferredValue 是通过 debounce 实现的”
它基于 Lane 优先级 + 时间切片,而不是定时器。新输入会直接打断旧 deferredValue 的渲染,debounce 做不到这种精细控制。
一句话总结
时间切片不是让渲染更快,而是让渲染”更懂礼貌”——做完一小块就问问:你需要我用主线程吗?需要我就让开。