巧用react调度思想实现requestIdleCallback

215 阅读3分钟

1、前言

2023年了js的执行会阻塞dom解析这个知识点应该无人不知无人不晓了,而js阻塞渲染会导致我们单页应用在首次加载时出现短暂的白屏。缩短白屏时间,也就是首屏优化的方案也很多,最行之有效的就是尽可能的减少或者延后JavaScript 的执行和下载。当有部分可以延后执行的js代码时,我们通常可以使用requestIdleCallback来执行这段代码,将渲染线程的有限资源释放出来,用于html的渲染。

2、requestIdleCallback

image.png 图: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代码,将当前任务重新放入宏任务队列,并更新时间戳,等待下一次的执行时机到来即可。