requestIdleCallback是一个Web API,允许开发者安排在主线程空闲时执行的低优先级回调函数。这个函数的主要目的是使得开发者能够在不影响关键事件如动画和输入响应的情况下,执行后台或低优先级的任务。简单来说就是可以让我们在浏览器空闲的时候做一些事情。
解释下空闲时间
我们知道页面是一帧一帧绘制出来的,对于人眼来说,当每秒切换60张图片时,就会认为是连贯的。所以主流的显示器是60hz的,1s刷新60次,那么每16.7ms需要刷新一次, 16.7ms实际上指的是两帧画面的间隔时间,在这16ms内,先执行渲染,剩下的时间执行任务队列,这是理想状态。如果任务时间过长,下一帧的渲染一直不能执行,导致两帧时间大于16.7ms,页面就卡顿了
所以如果每帧要执行的东西的时间小于 16.7ms,就说明有空闲时间可以利用,如下图所示(来源 W3C):
requestIdleCallback的模拟
可惜的是requestIdleCallback兼容性不够好,Safari完全不支持
参考react的实现,我们可以使用requestAnimationFrame和MessageChannel来模拟实现一个
requestAnimationFrame在每一帧开始渲染前执行,当帧开始渲染前,我们标记开始时间(start),并使用MessageChannel创建一个宏任务,根据Event Loop流程,渲染完毕后,会执行刚才创建出的宏任务,这时在宏任务中判断当前帧渲染耗费的时间(current - start),判断渲染耗时是否小于16.7ms(current - start < 16.7),来判断当前是否是空闲时间。
在这里有个疑问 requestAnimationFrame是宏任务还是微任务?
requestAnimationFrame 就是个宏任务,因为每个 requestAnimationFrame 执行完也都会把控制权交给浏览器,也正是因为这个特点,才可以使用 requestAnimationFrame 进行大数据渲染的性能优化,而不是使用微任务。requestAnimationFrame 本身的设计就是在每一帧渲染完准备显示之前去执行的,也就是 requestAnimationFrame 执行的时候浏览器本身就准备显示新的内容了,如果 requestAnimationFrame 像微任务那样执行完不把控制权交还给浏览器,那么这个设计就不合理了,所以 requestAnimationFrame 的设计天然就是执行完之后就需要把控制权交还给浏览器,让浏览器去渲染新一帧的内容。
利用 requestAnimationFrame + MessageChannel 实现
let deadlineTime // 当前帧结束时间
let callback // 需要回调的任务
let channel = new MessageChannel(); // postMessage 的一种,该对象实例有且只有两个端口,并且可以相互收发事件,当做是发布订阅即可。
let port1 = channel.port1;
let port2 = channel.port2;
port2.onmessage = () => {
// 是否超时
const currentDidTimeout = () => deadlineTime - performance.now();
if (currentDidTimeout() > 1 && callback) {
const deadline = { timeRemaining, didTimeout: false };
callback(deadline);
}
}
window.requestIdleCallback = function(cb) {
requestAnimationFrame(timeStamp => {
// 过期时间 = 默认这是一帧的开始时间 + 一帧大概耗时
deadlineTime = timeStamp + 16.7
callback = cb
port1.postMessage(null);
});
}
适合的场景
预处理,例如当你需要处理一些数据,但这些数据不需要立即展示给用户时,可以在空闲时预处理这些数据。
延迟执行: 当你有一些非必须立刻执行的代码时,比如初始化某些非关键的组件,你可以使用它来推迟这些任务的执行。
埋点日志相关,对于跟踪和分析网站使用情况的场景,通常可以在空闲时执行,以减少影响用户体验的风险。
使用requestIdleCallback的目的是确保关键任务(如处理用户输入、动画等)能够不受干扰地顺滑运行,而将非关键任务推迟到浏览器有足够资源处理它们的时候。这样既提高了页面性能,又优化了用户体验。需要注意的是,并不是所有的后台任务都适合用requestIdleCallback来处理;
不适合的场景
不适合操作dom&更新UI 因为执行时机不确定可能导致视觉难以预测,而且requestIdleCallback是在渲染完成才调用的 可能会引发回流重绘。
不适合做一些耗时的长任务。虽然是在浏览器空闲执行 但依然运行在主线程上 耗时的长任务同样会导致帧率降低, 造成页面卡顿。