🤔日拱一卒:页面上有100万个任务需要执行,如何保证页面不卡顿

1,239 阅读4分钟

这是专栏面试官系列之一,主要是在网上整理一些我认为比较有意义的面试题,主要做些分享和记录

如何保证页面不卡顿

要在页面上同时执行100万个任务而不造成卡顿,关键是要合理利用浏览器的几个方面

1.异步执行能力 2.优化任务调度 3.每次执行的任务量。

1. 任务分批执行

  • 分割任务:将100万个任务分成多个小批次,每次只执行少量任务(比如每次执行1000个任务)。这样可以避免一次性执行所有任务导致浏览器线程阻塞。使用 setTimeout 或 setInterval 来分配任务,这样每个批次的任务执行后,浏览器可以有机会进行渲染和其他重要操作,从而减少卡顿。
   const tasks = Array.from({ length: 1000000 }, (_, i) => i); // 模拟任务数组
    function processTasksInChunks() {
      if (tasks.length === 0) return;

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

      setTimeout(processTasksInChunks, 0); // 在下一个事件循环中继续处理
    }

    processTasksInChunks();

因为setTimeout是在事件循环中是进入到延时队列的,所以会等待主线程的任务清空再执行延时队列中的任务,不会阻塞主线程的执行,也就不会造成卡顿。

2. 使用 Web Workers

  • 将任务移到单独的线程中执行,避免阻塞主线程。
  • 代码示例: 主线程:

const worker = new Worker('worker.js');
worker.postMessage({ tasks: Array.from({ length: 1000000 }, (_, i) => i) });

worker.onmessage = function (event) {
  console.log('Task completed:', event.data);
};

worker.js:


onmessage = function (event) {
  const tasks = event.data.tasks;
  tasks.forEach(task => {
    // 模拟耗时任务
  });
  postMessage('All tasks completed');
};

简单来说想要保证页面不卡顿,那就是不占用主线程,那么重新开一个单独的线程是一个很好的解决办法,但不能直接操作 DOM,需要和主线程进行通信,这增加了开发复杂度

3. 使用 requestIdleCallback

requestIdleCallback 是浏览器的API,可以在主线程空闲时执行非紧急任务。能高效利用主线程的空闲时间,而不会影响页面卡顿

const tasks = Array.from({ length: 1000000 }, (_, i) => i);
function processTasksWithIdle(deadline) {
  while (deadline.timeRemaining() > 0 && tasks.length > 0) {
    const task = tasks.shift();
    console.log(task);
  }

  if (tasks.length > 0) {
    requestIdleCallback(processTasksWithIdle);
  }
}
requestIdleCallback(processTasksWithIdle);

这个写法和settimeout的是一样的,其实原理也差不多,settimeout是通过事件循环来解决主线程长期被占用,而requestIdleCallback会自动判断主线程的空闲时间,但浏览器支持情况有限,某些环境下需要降级方案,对于实时性要求特别高的任务不支持

4. 虚拟Dom

浏览器需要为每个 DOM 节点分配内存、计算布局(Layout)、绘制样式(Paint)和响应事件。当节点数量过多(比如上万或百万个),浏览器在这些步骤上会消耗大量资源,从而导致卡顿


import { Virtuoso } from 'react-virtuoso';

const tasks = Array.from({ length: 1000000 }, (_, i) => `Task ${i}`);

<Virtuoso
  totalCount={tasks.length}
  itemContent={(index) => <div>{tasks[index]}</div>}
/>;

与上面其他几个不同的是,如果任务的表现形式是渲染大量 DOM 节点,这个时候需要采用虚拟列表来解决,只渲染当前可见区域的任务节点,这样浏览器会节省大量资源

总结

  • setTimeout/setInterval
    优点: 比较简单,可以轻松分批执行任务,保证页面不会完全卡顿。兼容性也非常好,几乎所有环境都支持。
    缺点: 比较粗糙,无法动态插入高优先级任务,可能导致某些重要任务的延迟处理。

  • requestIdleCallback
    优点: 专为非紧急任务设计,能够智能利用浏览器的空闲时间执行任务。这种方式可以很好地平衡性能和任务处理,页面的流畅性会更高。
    缺点: 浏览器支持情况有限,可能需要降级方案。对于实时性要求特别高的任务不是最佳选择。

  • Web Workers
    优点: 能将耗时任务移到单独的线程中,彻底避免主线程被阻塞,适合处理计算密集型的复杂任务。
    缺点: Web Workers 不能直接操作 DOM,需要和主线程进行通信,增加开发复杂度。

  • 虚拟滚动
    优点: 只作用于渲染大量数据的场景,只加载用户当前可见的部分数据,显著减少 DOM 节点数量和性能开销。
    缺点: 仅适用于列表渲染类的任务。如果任务逻辑和 DOM 操作无关就搞不定。

如果觉得有趣或有收获,请关注我的更新,给个喜欢和分享。您的支持是我写作的最大动力!
往期好文推荐