浅析requestIdleCallback

  我们都知道React 16实现了新的调度策略(Fiber),新的调度策略提到的异步、可中断,其实就是基于浏览器的 requestIdleCallback 和 requestAnimationFrame 这两个API。

  requestAnimationFrame就不多说可以看刚刚这篇博客:浅析requestAnimationFrame让你更加了解动画

  那么什么是requestIdleCallback?当关注用户体验,不希望因为一些不重要的任务(如统计上报)导致用户感觉到卡顿的话,就应该考虑使用requestIdleCallback。

  因为requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态

requestIdleCallback will schedule work when there is free time at the end of a frame, or when the user is inactive.

  我们先来了解一些相关背景知识。

一、页面流畅与 FPS

  我们都知道页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。

  1s 60帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16ms。

  那么浏览器每一帧(Frame)都需要完成哪些工作?

  通过上图可看到,一帧内需要完成如下六个步骤的任务:
  • 处理用户的交互,如点击、触碰、滚动等事件
  • JS 解析执行
  • 帧开始。窗口尺寸变更,页面滚动等的处理
  • requestAnimationFrame(rAF)
  • 布局
  • 绘制

二、为什么需要 requestIdleCallback ?

  在网页中,有许多耗时但是却又不能那么紧要的任务。它们和紧要的任务,比如对用户的输入作出及时响应的之类的任务,它们共享事件队列。如果两者发生冲突,用户体验会很糟糕。我们可以使用setTimout,对这些任务进行延迟处理。但是我们并不知道,setTimeout在执行回调时,是否是浏览器空闲的时候。

  而requestIdleCallback就解决了这个痛点,requestIdleCallback会在帧结束时并且有空闲时间,或者用户不与网页交互时,执行回调。

1、空闲时间

  requestIdleCallback 的callback会在浏览器的空闲时间运行,那么什么是空闲时间呢?

  空闲时间分两种:

(1)第一种:

  如上图:当我们在执行一段连续的动画的时候,第一帧已经渲染到屏幕上了,到第二帧开始渲染,这段时间内属于空闲时间。这种空闲时间会非常的短暂,如果我们的屏幕是60hz(1s内屏幕刷新60次)的。那么空闲时间会小于16ms(1000ms / 16)。

(2)第二种:

  另外一种空闲时间,当用户属于空闲状态(没有与网页进行任何交互),并且屏幕中也没有动画执行。此时空闲时间是无限长的。但是为了避免不可预测的事(用户突然和网页进行交互),空闲时间最大应该被限制在50ms以内。

为什么最大是50ms?人类对100ms内的响应会认为是瞬时的。将空闲时间限制在50ms以内,是为了避免,空闲时间内执行任务,从而导致了对用户操作响应的阻塞,使用户感到明显的响应滞后。

  在空闲期间,callback的执行顺序是以FIFO(先进先出)的顺序。但是如果在空闲时间内依次执行callback时,有一个callback的执行时间,已经将空闲时间用完了,剩下的callback将会在下一次的空闲时间执行。

三、requestIdleCallback

  上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务。

  从上图也可看出,和 requestAnimationFrame 每一帧必定会执行不同,requestIdleCallback 是捡浏览器空闲来执行任务。如此一来,假如浏览器一直处于非常忙碌的状态,requestIdleCallback 注册的任务有可能永远不会执行。此时可通过设置 timeout (见下面 API 介绍)来保证执行。

1、API

var handle = window.requestIdleCallback(callback[, options])
  • callback:回调,即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:
    • didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。
    • timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。
  • options:目前 options 只有一个参数
    • timeout。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲。
IdleDeadline对象参考MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/IdleDeadline

2、代码示例

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('执行任务');
}
  超时的情况,其实就是浏览器很忙,没有空闲时间,此时会等待指定的 timeout 那么久再执行,通过入参 dealine 拿到的 didTmieout 会为 true,同时 timeRemaining () 返回的也是 0。超时的情况下如果选择继续执行的话,肯定会出现卡顿的,因为必然会将一帧的时间拉长。

3、cancelIdleCallback

  与 setTimeout 类似,返回一个唯一 id,可通过 cancelIdleCallback 来取消任务。

4、requestIdleCallback和requestAnimationFrame有什么区别?

  requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。

  我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60的时候是比较流畅的,而FPS为个位数的时候就属于用户可以感知到的卡顿了,那么在一帧里面浏览器都要做哪些事情呢?

  根据前面的图例我们知道一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。

  假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调。

  当程序栈为空页面无需更新的时候,浏览器其实处于空闲状态,这时候留给requestIdleCallback执行的时间就可以适当拉长,最长可达到50ms,以防出现不可预测的任务(用户输入)来临时无法及时响应可能会引起用户感知到的延迟。

  由于requestIdleCallback利用的是帧的空闲时间,所以就有可能出现浏览器一直处于繁忙状态,导致回调一直无法执行,这其实也并不是我们期望的结果(如上报丢失),那么这种情况我们就需要在调用requestIdleCallback的时候传入第二个配置参数timeout了?

  如果是因为timeout回调才得以执行的话,其实用户就有可能会感觉到卡顿了,因为一帧的执行时间必然已经超过16ms了。

5、requestIdleCallback里面可以执行DOM修改操作吗?

  强烈建议不要,从上面一帧的构成里面可以看到,requestIdleCallback回调的执行说明前面的工作(包括样式变更以及布局计算)都已完成。如果我们在callback里面做DOM修改的话,之前所做的布局计算都会失效,而且如果下一帧里有获取布局(如getBoundingClientRect、clientWidth)等操作的话,浏览器就不得不执行强制重排工作,这会极大的影响性能。

  另外由于修改dom操作的时间是不可预测的,因此很容易超出当前帧空闲时间的阈值,故而不推荐这么做。

  推荐的做法是在requestAnimationFrame里面做dom的修改,可以在requestIdleCallback里面构建Document Fragment,然后在下一帧的requestAnimationFrame里面应用Fragment。

  除了不推荐DOM修改操作外,Promise的resolve(reject)操作也不建议放在里面,因为Promise的回调会在idle的回调执行完成后立刻执行,会拉长当前帧的耗时,所以不推荐。推荐放在requestIdleCallback里面的应该是小块的(microTask)并且可预测时间的任务。

6、requestIdleCallback的兼容情况

7、总结

(1)一些低优先级的任务可使用 requestIdleCallback 等浏览器不忙的时候来执行,同时因为时间有限,它所执行的任务应该尽量是能够量化,细分的微任务(micro task)。

(2)不能在 requestIdleCallback 里进行dom操作。因为它发生在一帧的最后,此时页面布局已经完成,所以不建议在 requestIdleCallback 里再操作 DOM,这样会导致页面再次重绘。

(3)DOM 操作建议在 rAF 中进行。同时,操作 DOM 所需要的耗时是不确定的,因为会导致重新计算布局和视图的绘制,所以这类操作不具备可预测性。

(4)Promise 也不建议在这里面进行,因为 Promise 的回调属性 Event loop 中优先级较高的一种微任务,会在 requestIdleCallback 结束时立即执行,不管此时是否还有富余的时间,这样有很大可能会让一帧超过 16 ms。

(5)推荐使用npm包request-idle-callback

posted @ 2020-12-02 15:53  古兰精  阅读(2601)  评论(0编辑  收藏  举报