大家好!作为一名前端开发工程师,我们在日常工作中或多或少都会遇到这样的问题:页面中有大量的异步任务需要执行,但这些任务却导致页面出现卡顿、响应变慢,甚至让用户感到非常不适。今天我们就来探讨一个非常实用的话题——如何在不阻塞页面渲染的情况下高效执行大量任务。
1、为什么页面会卡顿?
在深入解决问题之前,我们先了解一下浏览器的渲染原理。浏览器的核心是单线程模型,所有的 JavaScript 代码和 DOM 操作都是在主线程中运行的。而页面的渲染(包括小球动画)则是在 UI 线程中完成的。
当主线程被大量任务占用时,UI 线程就会等待任务完成才能继续渲染页面,这就是页面卡顿的根本原因。因此,我们需要找到一种方法,让主线程的任务尽可能少地阻塞 UI 线程,同时还能高效地完成任务。
2、如何保证画面流畅的同时执行复杂任务
在浏览器中,所有的任务都被放入任务队列中。根据优先级的不同,任务分为宏队列(Macrotask Queue)和微队列(Microtask Queue)。
宏队列包含那些需要立即执行的任务,比如 setTimeout、setImmediate 等。
微队列则处理一些较小的任务,比如 promise 的回调和 postMessage。
在浏览器渲染的过程中,每当一个任务完成之后,浏览器会先执行微队列中的任务。只有当所有微队列的任务都完成后,才会回到宏队列继续执行。
因此,如果我们能将任务拆分成多个微队列任务,就能让页面渲染更加流畅。
2.1 使用 requestAnimationFrame
requestAnimationFrame
是一个非常强大的 API,它兼容性比较好,而且允许我们在每一帧绘制之前执行 JavaScript 代码。由于它属于微队列的一部分,因此可以有效地减少主线程的阻塞。
代码示例:
function heavyTask() {
console.log('执行繁重任务');
// 模拟耗时操作
for (let i = 0; i < 1000000; i++) {
// 这里可以放置实际的任务逻辑
}
}
function main() {
runTask(() => heavyTask());
console.log('任务完成');
}
function runTask(task) {
return new promise((resolve) => {
_run(task, resolve);
})
}
// 辅助函数 _run 实现了具体的任务分批和定时执行逻辑。它通过 requestAnimationFrame 控制任务的执行频率,确保每秒不超过60次(即16.6毫秒一帧)。
function _run(task, callback) {
// performance.now() 返回值表示为从time origin之后到当前调用时经过的时间
const start = performance.now();
requestAnimationFrame(() => {
try {
if (performance.now() - start < 16.6) {
task(); // 执行任务
callback(); // 调用回调函数,通知任务完成
} else {
_run(task, callback); // 如果时间不足,递归重新尝试
}
} catch (error) {
console.error('任务执行失败:', error);
callback && callback();
}
});
}
main(); // 启动主程序
performance.now():这个时间戳实际上并不是高精度的。为了降低像Spectre这样的安全威胁,各类浏览器对该类型的值做了不同程度上的四舍五入处理。(Firefox 从 Firefox 59 开始四舍五入到 2 毫秒精度)一些浏览器还可能对这个值作稍微的随机化处理。这个值的精度在未来的版本中可能会再次改善;浏览器开发者还在调查这些时间测定攻击和如何更好的缓解这些攻击。详细内容见MDN
在此基础上还可以进行进一步优化和改进。
2.1.1 增加错误处理
在实际应用中,应该添加错误捕捉机制,防止因任务执行失败而导致程序崩溃。可以在 _run
函数内部使用 try-catch
块来捕捉和处理异常。
function _run(task, callback) {
const start = performance.now();
requestAnimationFrame(() => {
try {
if (performance.now() - start < 16.6) {
task(); // 执行任务
callback(); // 调用回调函数,通知任务完成
} else {
_run(task, callback); // 如果时间不足,递归重新尝试
}
} catch (error) {
console.error('任务执行失败:', error);
callback && callback();
}
});
}
2.1.2 增加取消机制
在某些情况下,可能需要中途取消正在执行的任务,可以引入一个标志变量来控制任务的继续或终止。
let taskCancelled = false;
function cancelTask() {
taskCancelled = true;
}
function _run(task, callback) {
if (taskCancelled) return; // 如果已经被取消,退出
const start = performance.now();
requestAnimationFrame(() => {
if (performance.now() - start < 16.6) {
task(); // 执行任务
callback(); // 调用回调函数,通知任务完成
} else {
_run(task, callback); // 如果时间不足,递归重新尝试
}
});
}
2.1.3 限制总执行次数
可以在 _run
函数中增加计数器,限制总的执行次数,防止无限循环。
let taskExecutionCount = 0;
const maxExecutions = 10; // 最大允许的执行次数
function _run(task, callback) {
if (taskExecutionCount >= maxExecutions) {
console.log('任务已达到最大执行次数,停止执行');
return;
}
taskExecutionCount++;
const start = performance.now();
requestAnimationFrame(() => {
if (performance.now() - start < 16.6) {
task(); // 执行任务
callback && callback();
} else {
_run(task, callback); // 如果时间不足,递归重新尝试
}
});
}
2.2 使用requestIdleCallback
这个API用于安排在浏览器空闲时间执行的回调函数。它的主要目的是优化性能,将低优先级的任务推迟到空闲时间段处理,从而减少对主线程的压力,提升页面响应速度。
不过requestIdleCallback
的兼容性不如requestAnimationFrame
,必要时使用polyfill来保证跨浏览器兼容。
function runTask(task) {
return new promise((resolve) => {
_run(task, resolve);
})
}
function _run(task, callback) {
requestIdleCallback(deadline => {
try {
if (deadline.timeRemaining() > 0) {
task(); // 执行任务
callback(); // 调用回调函数,通知任务完成
} else {
_run(task, callback); // 如果时间不足,递归重新尝试
}
} catch (error) {
console.error('任务执行失败:', error);
callback && callback();
}
});
}
requestIdleCallback.timeRemaining()
:用来判断用户代理预计还剩余多少闲置时间,更多相关内容请访问MDN IdleDeadline
2.3 使用 Web Workers
对于非常耗时的任务,可以考虑将其委托给 Web Workers。Web Workers 是运行在后台线程中的脚本,不会阻塞主线程。
// 创建 Web Worker
const worker = new Worker('worker.js');
// 监听消息
worker.onmessage = (event) => {
console.log('任务完成');
};
// 发送任务
worker.postMessage({ task: 'compute' });
// worker.js 文件内容:
onmessage = function(event) {
const { task } = event.data;
if (task === 'compute') {
heavyTask();
postMessage('complete');
}
};
function heavyTask() {
for (let i = 0; i < 1000000; i++) {}
}
Web Workers:将耗时任务委托给后台线程,不会阻塞主线程。
postMessage:用于在主线程和 Web Worker 之间通信。
3、如何选择适合的方案?
如果你需要频繁执行小型任务,并希望它们尽可能不阻塞页面渲染,可以使用 requestAnimationFrame
或 requestIdleCallback
。
如果你的任务非常耗时且与主线程无关(比如图像处理、视频解码),可以考虑使用 Web Workers
。
4、总结
通过将任务拆分成微队列任务或委托给后台线程,我们可以有效地减少主线程的负担,从而让页面渲染更加流畅。记住,在前端开发中,性能优化是一个长期的过程,只有不断学习和实践,才能写出更高效、更用户友好的代码。
希望这篇文章能对你有所帮助!如果有什么问题,随时告诉我哦!😊
5、参考文献
- MDN-IdleDeadline:https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline
- MDN-requestIdleCallback:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
- MDN-Performance.now():https://developer.mozilla.org/zh-CN/docs/Web/API/Performance/now
- Window:requestAnimationFrame() 方法:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame
评论区