这是专栏面试官系列之一,主要是在网上整理一些我认为比较有意义的面试题,主要做些分享和记录
如何保证页面不卡顿
要在页面上同时执行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 操作无关就搞不定。