requestIdleCallback
requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。 我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60(每一秒刷新60次)的时候是比较流畅的,而FPS为个位数的时候就属于用户可以感知到的卡顿了,FPS60意味着:1000ms/60≈16.67,即每一帧的时间约为16.67ms(默认刷新率为60FPS) 那么在一帧里面浏览器都要做哪些事情呢,如下所示:
图中一帧包含了:处理用户的交互;JS解析执行;帧开始。窗口尺寸变更,页面滚去等的处理;rAF(即window.requestAnimationFrame);布局计算;页面重绘。 假如执行完这些操作用不了16.67ms,那么意味着这一帧有空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调,如下图所示:
当用户属于空闲状态(没有与网页进行任何交互),并且没有屏幕中也没有动画执行。此时空闲时间是无限长的。但是为了避免不可预测的事(用户突然和网页进行交互),空闲时间最大应该被限制在50ms以内。如下图所示:
上图可以看到当用户开始输入时,此时不再是空闲,但也要等该callback执行完才开始新的一帧,当然这时间间隔很短并不影响体验。
API
var handle = window.requestIdleCallback(callback[, options])
- callback: ():回调即空闲时需要执行的任务,接收一个 IdleDeadline 对象作为入参。其中 IdleDeadline 对象包含:
- didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。
- timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。
- options:目前 options 只有一个参数
- timeout 。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲。
由于requestIdleCallback利用的是帧的空闲时间,所以就有可能出现浏览器一直处于繁忙状态,导致回调一直无法执行,此时需要传入参数timeout了,如果是因为timeout回调才得以执行的话,其实用户就有可能会感觉到卡顿了。 【example】
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });
const tasks = [
() => {
console.log("第一个任务");
},
() => {
console.log("第二个任务");
},
() => {
console.log("第三个任务");
},
];
function myNonEssentialWork (deadline) {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
work();
}
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
function work () {
tasks.shift()();
console.log('执行任务');
}
requestIdleCallback里面可以执行DOM修改操作吗?
强烈建议不要,从上面一帧的构成里面可以看到,requestIdleCallback回调的执行说明前面的工作(包括样式变更以及布局计算)都已完成。如果我们在callback里面做DOM修改的话,之前所做的布局计算都会失效,而且如果下一帧里有获取布局(如getBoundingClientRect、clientWidth)等操作的话,浏览器就不得不执行强制重排工作,这会极大的影响性能,另外由于修改dom操作的时间是不可预测的,因此很容易超出当前帧空闲时间的阈值,故而不推荐这么做(假设超过阈值,将会拉长一帧的长度,导致帧率下降,用户感觉明显的卡顿)。 除了不推荐DOM修改操作外,Promise的resolve(reject)操作也不建议放在里面,因为Promise的回调会在idle的回调执行完成后立刻执行,会拉长当前帧的耗时,所以不推荐。 一些低优先级的任务可使用 requestIdleCallback 等浏览器不忙的时候来执行,同时因为时间有限,它所执行的任务应该尽量是能够量化,细分的微任务(micro task)。
shim
该方法目前没有polyfill,以下是shim:
window.requestIdleCallback =
window.requestIdleCallback ||
function (cb) {
var start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
}
window.cancelIdleCallback =
window.cancelIdleCallback ||
function (id) {
clearTimeout(id);
}
【参考文章】
- 事件循环
- 你应该知道的requestIdleCallback
- 详解 requestIdleCallback
- requestIdleCallback和requestAnimationFrame详解
- Using requestIdleCallback
|