1、前言
2023年了js的执行会阻塞dom解析这个知识点应该无人不知无人不晓了,而js阻塞渲染会导致我们单页应用在首次加载时出现短暂的白屏。缩短白屏时间,也就是首屏优化的方案也很多,最行之有效的就是尽可能的减少或者延后JavaScript 的执行和下载。当有部分可以延后执行的js代码时,我们通常可以使用requestIdleCallback来执行这段代码,将渲染线程的有限资源释放出来,用于html的渲染。
2、requestIdleCallback
图:requestIdleCallback兼容性
requestIdleCallback兼容性在移动端并不理想,所以我们需要自定义实现requestIdleCallback的能力。
requestIdleCallback 触发时机说明: 动画由一帧一帧图片构成,刷新图片的频率决定了动画的流畅程度,也就是我们游戏里常说的fps低,卡得要死。而大部分显示设备的fps在60fps,60FPS表示运行时每个画面执行时间为1/60秒,而在这1/60秒内,浏览器渲染流水线流程要处理的任务可以分为:
- 1、构建 DOM 树
- 2、样式计算
- 3、布局
- 4、分层
- 5、绘制
- 6、分块
- 7、光栅化
- 8、合成
当这些任务完成后,有时因为执行任务过久导致超过了1/60秒,就会导致fps下降,反之我们执行这些任务未用完1/60秒,那么剩余的时间就可以用来执行一些不是那么紧急的任务,也就是requestIdleCallback所要执行的任务。
3、实现demo
function generatorDeferred() {
let defferred = {};
defferred.promise = new Promise((resolve, reject) => {
defferred.resolve = resolve;
defferred.reject = reject;
});
return defferred;
}
function performTaskWhileIdle(task) {
// 记录任务生成时间
let time = Date.now();
// 生成promise信号
let deferred = generatorDeferred();
return (...args) => {
// 执行调度任务
const flush = () => {
const now = Date.now();
// 判断距离任务设置下去到被唤起时,渲染线程花费了多久时间(时间越久,当前待执行任务越多,需要暂缓执行)
if (now - time > 5) {
// 更新任务生成时间
time = now;
// 暂缓执行,重新生成调度任务放入宏任务队列
schedule();
} else {
// 执行任务,并将执行结果通过promise返回
deferred.resolve(task.apply(this, args));
}
};
// 生成调度任务
const schedule = () => {
if (MessageChannel) {
const { port1, port2 } = new MessageChannel();
port1.addEventListener('message', function l() {
flush();
port1.removeEventListener('message', l);
});
port1.start();
port2.postMessage(null);
} else {
setTimeout(flush);
}
};
schedule();
return deferred.promise;
};
}
// 使用方法
(function () {
const task = (params) => {
return params;
};
performTaskWhileIdle(task)('执行任务').then(res => {
console.log(res);
});
})();
4、实现逻辑
想要实现requestIdleCallback的核心问题,就是我们怎么判断当前渲染线程是否空闲?
js是通过事件循环的方式,不停触发我们的js代码,那么想判断渲染线程是否空闲,自然要从事件循环的机制入手,我们都知道js首先执行同步代码,执行过程中如果有宏任务,那么就将宏任务放入队列,如果有微任务产生就将微任务放入队列,而后从宏任务队列取出一个宏任务执行,执行完成后再清空微任务队列,此时再根据页面是否需要渲染,是否有vSync信号,来判断调用requestAnimationFrame。 在这样的循环机制当中,我们可以在生成宏任务时,记录下当前的时间戳,此时将需要执行的宏任务压入队列。当此宏任务被调用时,判断下此时时间戳与上一次时间戳的差值,如果当前渲染线程繁忙,显然会导致我们需要执行的宏任务执行时机被延后,那么此时差值会偏大可能大于5ms,这种情况,我们就可以暂时不执行需要执行的js代码,将当前任务重新放入宏任务队列,并更新时间戳,等待下一次的执行时机到来即可。