背景
requestIdleCallback这个api可以说是由react带火的 react 18的并发模式 背后就有requestIdleCallback的影子 但是因为一些原因导致react不得不自己实现了一版 所以我们今天来深入了解下这个api可以解决哪些问题 它到底有怎样的魔力~
先有问题再有答案
requestIdleCallback是做什么的
浏览器什么时候空闲
requestIdleCallback里面可以做哪些任务 不能做哪些任务?
兼容性如何
如何实现一个requestIdleCallback方法
requestIdleCallback简介
requestIdleCallback是一个Web API,允许开发者安排在主线程空闲时执行的低优先级回调函数。这个函数的主要目的是使得开发者能够在不影响关键事件如动画和输入响应的情况下,执行后台或低优先级的任务。
关于requestIdleCallback的关键概念:
- 回调函数: 回调函数是在主线程空闲时被调用的函数。每次调用时,都会传入一个IdleDeadline对象,该对象提供一个timeRemaining()方法,用来检测当前帧中剩余的空闲时间。
- 空闲时间和截止时间(deadline) : IdleDeadline对象的timeRemaining()方法返回一个DOMHighResTimeStamp,表示在执行回调函数时,在当前帧中剩余多少空闲时间(毫秒)。开发者可以使用这个时间来执行任务,并在时间耗尽前选择适当的时机终止任务,从而避免影响关键渲染或事件处理。
- 调度和取消回调: requestIdleCallback函数安排一个回调函数在主线程下一次空闲时被执行,并返回一个ID,可以用这个ID通过cancelIdleCallback函数取消回调。
- 超时: 你还可以给requestIdleCallback传递一个对象,其中一个属性是timeout,用来指定最长时间(毫秒)。如果任务在指定的时间内尚未执行,即使主线程不空闲,浏览器也会尽量执行回调
使用示例:
requestIdleCallback((deadline) => {
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) {
// 执行任务
performTask(tasks.shift());
}
}, { timeout: 2000 });
浏览器什么时间空闲?
要回答这个问题 首先我们要了解一些前置知识
前置文章:
js三座大山之异步五基于异步的js性能优化
浏览器:帧原理&渲染优化的基石
帧率:浏览器每秒渲染60帧,如果低于这个频率就会失帧,页面不会发生变化,造成的现象就是卡了~。fps指每秒钟页面呈现的帧数,通常用于衡量页面的流畅程度。
浏览器一帧:不断的执行js,在帧末会开始渲染流程绘制UI。
如果渲染结束 时间还有剩余 那么浏览器会调用requestIdleCallback 这个api执行对应的回调函数。
所以requestIdleCallback的调用时机是不确定的 每帧结束没有剩余时间就不会被执行 可能会间隔很久.
应用场景
因为requestIdleCallback的调用特性,决定了它的使用场景。
适合的场景
- 预处理,例如当你需要处理一些数据,但这些数据不需要立即展示给用户时,可以在空闲时预处理这些数据。
- 埋点日志相关,对于跟踪和分析网站使用情况的代码,通常可以在空闲时执行,以减少影响用户体验的风险。
- 延迟执行: 当你有一些非必须立刻执行的代码时,比如初始化某些非关键的UI组件,你可以使用 requestIdleCallback 来推迟这些任务的执行。
使用requestIdleCallback的目的是确保关键任务(如处理用户输入、动画等)能够不受干扰地顺滑运行,而将非关键任务推迟到浏览器有足够资源处理它们的时候。这样既提高了页面性能,又优化了用户体验。需要注意的是,并不是所有的后台任务都适合用requestIdleCallback来处理;
不适合的场景
- 不适合操作dom&更新UI 因为执行时机不确定可能导致视觉难以预测,而且requestIdleCallback是在渲染完成才调用的 可能会引发回流重绘。
- 不适合做一些耗时的长任务。虽然是在浏览器空闲执行 但依然运行在主线程上 耗时的长任务同样会导致帧率降低, 造成页面卡顿。
兼容性
safari: 我就不支持 你能咋地…..
如何实现一个requestIdleCallback
我们需要解决以下几个问题:
- 如何估算浏览器当前是否空闲
- 如何让出主线程避免主线程被占用
思路:
浏览器一帧执行正常是16.6ms 如果执行时间大于这个值 可以任务浏览器处于繁忙状态。
否则即代表空闲。
因为requestAnimationFrame
这个函数是和渲染保持同步的 可以通过函数获取帧的开始时间,然后使用帧率(开始时间+16.6ms)计算出帧的结束时间, 然后开启一个宏任务,当宏任务被执行时 比较当前的执行时间和帧结束的时间 判断出当前帧是否还有空闲…
因为是宏任务不会像微任务优先级那么高,可以被推迟到下一个事件循环中不会阻塞渲染。我们这里使用MessageChannel
宏任务来实现。
关于其他宏任务可以参考 js三座大山之异步七实现宏任务的N种方式。
/**
* polyfill requestIdleCallBack
* 当浏览器空闲时执行 避免阻塞主线程
* 通过raf + 帧执行时间 获得一帧结束时间
* 通过messageChannel使用宏任务让出主线程
* @param callback
* @param params
* @returns
*/
function idleExecute(callback: IdleCallBackFn, params?: Options) {
const channel = new MessageChannel(); // 建立宏任务的消息通道
const port1 = channel.port1;
const port2 = channel.port2;
const timeout = params?.timeout || -1;
let cb: IdleCallBackFn | null = callback;
let frameDeadlineTime = 0; // 当前帧结束的时间
const begin = performance.now();
let cancelFlag = 0;
const runner = (timeStamp: number) => {
// 获取当前帧结束的时间
frameDeadlineTime = timeStamp + frameTime;
if (cb) {
port1.postMessage('task')
}
}
port2.onmessage = () => {
const timeRemaining = () => {
const remain = frameDeadlineTime - performance.now();
return remain > 0 ? remain : 0;
};
let didTimeout = false;
if (timeout > 0) {
didTimeout = performance.now() - begin > timeout;
}
// 没有可执行的回调 直接结束
if (!cb) {
return;
}
// 当前帧没有时间&没有超时 下次再执行
if (timeRemaining() <= 1 && !didTimeout) {
cancelFlag = requestAnimationFrame(runner);
return;
}
//有剩余时间或者超时
cb({
didTimeout,
timeRemaining,
});
cb = null;
}
cancelFlag = requestAnimationFrame(runner)
return cancelFlag;
}
完整代码
原文链接:https://juejin.cn/post/7350140783548956683 作者:某某某人