Skip to content

时间分片 Time Slicing

浏览器时间分片是一种前端性能优化技术,用于将长时间运行的任务拆分为多个小块,分散到多个浏览器空闲时间片段中执行,从而避免长时间阻塞主线程,提升页面的响应性。

通过时间分片技术,任务的执行被分散到多个帧内,让浏览器在任务之间有时间处理其他重要的工作(如渲染、用户交互等)。

时间分片

单线程限制

JavaScript 在浏览器中是单线程执行的,长时间运行的任务(例如复杂的循环、计算等)会阻塞主线程,导致:

  • 页面卡顿
  • 用户事件(如点击、滚动等)得不到及时响应

提升用户体验

  • 将任务分片可以释放主线程,使浏览器能及时响应用户操作

实现方法

1️⃣ setTimeout

setTimeout 将任务切分成多个小块,在每次任务完成后交还控制权给浏览器。

js
function bigTask() {
  const tasks = Array.from({ length: 1000000 }, (_, i) => i); // 模拟大量任务
  // [1, 2, ..., 9999]

  function processChunk() {
    const chunk = tasks.splice(0, 1000); // 每次处理 1000 个任务
    chunk.forEach((task) => {
      // 模拟任务处理
      console.log(task);
    });

    if (tasks.length > 0) {
      setTimeout(processChunk, 0); // 下一次时间片再处理
    }
  }

  processChunk();
}

bigTask();
  • 每次只处理 1000 个任务,然后将控制权交还给浏览器。
  • 下一次处理通过 setTimeout 执行。
  • 避免一次性执行 100 万个任务导致主线程卡死。

2️⃣ requestAnimationFrame

requestAnimationFrame 是专为与动画渲染同步设计的 API,每次屏幕刷新时执行回调,非常适合时间分片。

js
function bigTask() {
  const tasks = Array.from({ length: 1000000 }, (_, i) => i);

  function processChunk() {
    const chunk = tasks.splice(0, 1000); // 每帧处理 1000 个任务
    chunk.forEach((task) => {
      console.log(task);
    });

    if (tasks.length > 0) {
      requestAnimationFrame(processChunk); // 下一帧再处理
    }
  }

  processChunk();
}

bigTask();
  • 每帧处理一部分任务,利用浏览器刷新机制分散任务。
  • 保持页面动画和渲染流畅。

案例

初始化 n = 0,每次计算让 n += 1,直到 n = 1000。但每次计算的时间不能超过 15ms

js
// 这段代码通过 requestAnimationFrame 和 setTimeout 把累加任务分片
// 让每次执行一部分操作后,暂停一下,给浏览器喘息的时间。
const sumToN = (n) => {
  const start = Date.now();
  // 返回一个 Promise 对象
  return new Promise((resolve) => {
    let sum = 0; // 初始化累加和为 0
    let startTime = Date.now(); // 记录当前时间,后续用于计算时间间隔
    let intervalId; // 用于存储 setTimeout 返回的定时器 ID

    // 定义递归函数用于累加
    const addNext = () => {
      console.log("🚀🚀🚀 sum: ", sum); // 从 0 打印到 1000,大概需要花 8 秒钟
      if (sum < n) {
        // 判断是否还需要继续累加
        sum += 1; // 累加 1
        // requestAnimationFrame(addNext);
        // addNext();
        if (Date.now() - startTime < 15) {
          // 计算从上一次任务开始到现在的时间差
          // 如果从上次时间点开始的时间小于 15 毫秒,表示任务还没有占用太多时间
          // 继续使用 requestAnimationFrame 递归调用自身,让任务尽快执行
          requestAnimationFrame(addNext);
        } else {
          // 如果时间差大于等于 15 毫秒,表示任务已经消耗了一些性能
          // 切换到 setTimeout,让主线程有时间处理其他事情
          startTime = Date.now();
          intervalId = setTimeout(addNext, 0); // 延迟为 0,表示尽快执行
          // 通过这种方式,累加任务会被分片执行,而不会卡死主线程
        }
      } else {
        // 当累加完成时,清除定时器
        clearTimeout(intervalId);
        // 将最终的 sum 值通过 resolve 返回
        console.log("🚀🚀🚀 time: ", Date.now() - start);
        resolve(sum);
      }
    };

    // 开始执行递归函数
    addNext();
  });
};

// 使用示例
sumToN(1000).then((result) => {
  console.log("🚀🚀🚀 result: ", result); // 输出累加结果
});

不使用任何优化,直接累加,耗时大约 160ms;使用该方法后,耗时大约 8000ms。