文章

时间切片原理深度解析

时间切片原理深度解析

一句话概括

时间切片(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,用户看到残次品。

「其实你每天都在用」

  1. 无限滚动列表:往下滑加载更多数据,React 在帧空闲时间分批渲染新行,滚动帧率稳定 60fps。

  2. 搜索框输入过滤 50000 条数据useDeferredValue(query) 让输入框立即响应(SyncLane),过滤+渲染走 TransitionLane 分片执行。

  3. Tab 切换大数据页面startTransition 让 tab 按钮高亮立即生效,新 tab 内容后台切片渲染,快速切 tab 不卡。

  4. Chrome Performance 面板里的锯齿图:时间切片模式下,Main 线程的火焰图是多个短条(每个 ~5ms),不再是单一长条。

  5. 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 做不到这种精细控制。

一句话总结

时间切片不是让渲染更快,而是让渲染”更懂礼貌”——做完一小块就问问:你需要我用主线程吗?需要我就让开。

本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量