我们都知道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