requestIdleCallback 是一个还在实验中的 api,可以让我们在浏览器空闲的时候做一些事情
RequestIdleCallback 简单的说,判断一帧有空闲时间,则去执行某个任务。目的是为了解决当任务需要长时间占用主进程,导致更高优先级任务(如动画或事件任务),无法及时响应,而带来的页面丢帧(卡死)情况。故RequestIdleCallback 定位处理的是: 不重要且不紧急的任务。
基本语法
var handle = window.requestIdleCallback(callback[, options])
基本应用
type Deadline = {
timeRemaining: () => number // 当前剩余的可用时间。即该帧剩余时间。
didTimeout: boolean // 是否超时。
}
function work(deadline:Deadline) { // deadline 上面有一个 timeRemaining() 方法,能够获取当前浏览器的剩余空闲时间,
单位 ms;有一个属性 didTimeout,表示是否超时
console.log(`当前帧剩余时间: ${deadline.timeRemaining()}`);
if (deadline.timeRemaining() > 1 || deadline.didTimeout) {
// 走到这里,说明时间有余,我们就可以在这里写自己的代码逻辑
}
// 走到这里,说明时间不够了,就让出控制权给主线程,下次空闲时继续调用
requestIdleCallback(work);
}
requestIdleCallback(work, { timeout: 1000 }); // 这边可以传一个回调函数(必传)和参数(目前就只有超时这一个参数)
缺点
requestAnimationFrame 的回调会在每一帧确认执行, 属于高优先级任务. 而 requestIdleCallback 的回调不一定, 属于低优先级任务. 我们看到的页面是浏览器一帧一帧绘制出来的, 通常 FPS 在 60 的时候是比较流畅的, 而 FPS 比较低的时候就会感觉到卡顿. 那么在每一帧里浏览器会做哪些事情呢, 如下图所示:
图中一帧包括了用户的交互, JavaScript 脚本执行; 以及 requestAnimationFrame(rAF)的调用, 布局计算以及页面重绘等. 假如某一帧里执行的任务不多, 在不到 16.66ms(1000/60)内就完成了上述任务, 那么这一帧就会有一定空闲时间来执行 requestIdleCallback 的回调, 如图所示:
当程序栈为空页面无需更新的时候, 浏览器其实是处于空闲状态, 这时候留给requestIdleCallback执行的时间就可以适当拉长, 最长达到 50ms, 以防出现不可预测的任务(如用户输入), 避免无法及时响应使用户感知到延迟.
由于requestIdleCallback利用的是帧的空闲时间, 所以有可能出现浏览器一直处于繁忙状态, 导致回调一直无法执行, 那这时候就需要在调用requestIdleCallback的时候传递第二个配置参数timeout了.
requestIdleCallback(myNonEssentialWork, { timeout: 2000 });
function myNonEssentialWork(deadline) {
// 当回调函数是由于超时才得以执行的话,deadline.didTimeout为true
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0
) {
doWorkIfNeeded();
}
if (tasks.length > 0) {
requestIdleCallback(myNonEssentialWork);
}
}
如果是因为timeout回调才得以执行的话, 用户就有可能感受到卡顿, 因为一帧的时间已经超过 16ms 了.
不要做什么
推荐的做法是在 requestAnimationFrame里面做 DOM 的修改.
能做什么
此时我们就可以使用 requestIdleCallback 调度上报时机,避免上报阻塞页面渲染,下面是简单的代码示例(可跳过)
const queues = [];
const btns = btns.forEach(btn => {
btn.addEventListener('click', e => {
// do something
pushQueue({
type: 'click'
// ...
}));
schedule(); // 等到空闲再处理
});
});
function schedule() {
requestIdleCallback(deadline => {
while (deadline.timeRemaining() > 1) {
const data = queues.pop();
// 这里就可以处理数据、上传数据
}
if (queues.length) schedule();
});
}
function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
if (!navigator.onLine || isSlowNetwork) {
// Don't prefetch if in a slow network or offline
return;
}
requestIdleCallback(async () => {
const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
}
回过头来,如果 requestIdleCallback 长时间内没能得到执行,说明一直没有空闲时间,很有可能就是发生了卡顿,从而可以打点上报。它比较适用于行为卡顿,举个例子:点击某个按钮并同时添加我们的 requestIdleCallback 回调,如果点击后的一段时间内这个回调没有得到执行,很大概率是这个点击操作造成了卡顿。
用 setTimeout 实现
首选大家要知道一个前提,为什么能够 setTimeout 来模拟,所以我们先简单看下下面这两行代码:
// 某种程度上功能相似,写法也相似
requestIdleCallback(() => console.log(1));
setTimeout(() => console.log(2));
了解过 setTimeout 的同学应该知道这个东西它不准,上面那样写并不是立刻执行的意思,而是尽可能快的执行,就是等待主线程为空,微任务也执行完了,那么就可以轮到 setTimeout 执行了,所以 setTimeout(fn) 某种程度上讲也有空闲的意思,了解了这个点我们就可以用它来模拟啦,直接看下面的代码即可,就是在 setTimeout 里面多了个构造参数的步骤:
window.requestIdleCallback = function(cb) {
let start = Date.now();
return setTimeout(function () {
const deadline = { // 这边就是为了构造参数
timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), // 剩余时间我们写死在 50ms 内,也就是前面提到的上限值,其实你也可以写成 40、30、16、10 等😂
didTimeout: false // 因为我们不推荐使用 timeout 参数,所以这里就直接写死 false
};
cb(deadline);
});
}
要注意的是,这个并不是 requestIdleCallback 的 polyfill ,因为实际上它们并不相同。setTimeout 并不算是真正的利用空闲时间,而是在条件允许的情况下尽可能快的执行你的代码。上面的代码并不会像真正的 requestIdleCallback 那样将自己限制在这一帧的空闲时间内,但是它达到了两个效果,一个是将任务分段,一个是控制每次执行的时间上限。一般满足这两个条件的就是宏任务了,所以除了 setTimout 外,postMessage 也是可以实现的。接下来我们来看看模拟的另一种方法
用 requestAnimationFrame + MessageChannel 实现
let deadlineTime // 当前帧结束时间
let callback // 需要回调的任务
let channel = new MessageChannel(); // postMessage 的一种,该对象实例有且只有两个端口,并且可以相互收发事件,当做是发布订阅即可。
let port1 = channel.port1;
let port2 = channel.port2;
port2.onmessage = () => {
const timeRemaining = () => deadlineTime - performance.now();
if (timeRemaining() > 1 && callback) {
const deadline = { timeRemaining, didTimeout: false }; // 同样的这里也是构造个参数
callback(deadline);
}
}
window.requestIdleCallback = function(cb) {
requestAnimationFrame(rafStartTime => {
// 大概过期时间 = 默认这是一帧的开始时间 + 一帧大概耗时
deadlineTime = rafStartTime + 16
callback = cb
port1.postMessage(null);
});
}
上面这种方式会比 setTimeout 稍好一些,因为 MessageChannel 的执行在 setTimeout 之前,并且没有 4ms 的最小延时。那为什么不用微任务模拟呢?因为如果你用微任务模拟的话,在代码执行完之后,所有的微任务就会继续全部执行,不能及时的让出主线程。
ps:这两种方法都不是 polyfill,只是尽可能靠近 requestIdleCallback,并且剩余时间也是猜测的。
RequestIdleCallback 实验案例
结论:
let scheduledHostCallback = null;
let isMessageLoopRunning = false;
const channel = new MessageChannel();
// port2 发送
const port = channel.port2;
// port1 接收
channel.port1.onmessage = performWorkUntilDeadline;
const performWorkUntilDeadline = () => {
// 有执行任务
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Yield after `yieldInterval` ms, regardless of where we are in the vsync
// cycle. This means there's always time remaining at the beginning of
// the message event.
// 计算一帧的过期时间点
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
// 执行完该回调后, 判断后续是否还有其他任务
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
// 还有其他任务, 推进进入下一个宏任务队列中
port.postMessage(null);
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
// requestHostCallback 一帧中执行任务
requestHostCallback = function(callback) {
// 回调注册
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
// 进入宏任务队列
port.postMessage(null);
}
};
cancelHostCallback = function() {
scheduledHostCallback = null;
};
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。