参考文章

requestIdleCallback和requestAnimationFrame详解

网页渲染性能优化

被誉为神器的requestAnimationFrame

浏览器帧原理剖析

简介

requestAnimationFrame和requestIdleCallback的应用场景并不一样,requestAnimationFrame是为了实现更流畅和性能更好的动画;后者是为了在渲染空闲时间执行优先级不高的操作,以避免阻塞渲染。
两者放到一起进行说明是因为它们都是由浏览器控制执行时机,而不是由开发者通过定时器控制。另外,相对于不使用这两个方法,使用它们都能在一定的情况下获得性能的提升。

浏览器渲染一帧都做了什么

  1. 浏览器会定时刷新界面,不管js有没有改变dom,通常刷新频率和显示器帧率相同。
  2. 如果dom未发生变化,不会触发主线程的重新计算。
  3. 在刷新帧的间隔中,可能会触发一些元素事件,如输入文本点击按钮等,还可能有定时任务或者异步任务执行,还可能有注册的requestAnimationFrame的回调。执行完js后如果改变了DOM,就会触发主线程的重排或者重绘,计算dom、布局、分层、分块、栅格化一系列操作,然后重新绘制新的界面。
  4. 渲染完一帧后如果有空闲时间,会调用requestIdelCallback注册的回调。什么算是空闲呢?这和浏览器渲染的频率有关系。一般情况下浏览器渲染的频率会和显示器保持一致。通常1s 60帧的帧率可以让肉眼感觉不卡顿。1s ÷ 60 ≈ 16ms。我们以60fps为例,如果浏览器渲染一帧发现不到16ms,那么剩余时间就算是空闲时间。

总结一下,在每一帧渲染完成后,在下一帧绘制之前如果有requestAnimationFrame,则调用之。如果在一帧渲染完后有空闲,就会执行requestIdleCallback注册的回调。

渲染过程

requestAnimationFrame

通常我们可以通过定时器(setTimeout/setInterval)实现一个动画,但是定时器时间并不精确,如果时间太短,那么可能造成多余的操作,消耗CPU,如果时间长,就会导致动画不流畅。另外当画面不展示时候,定时器依然执行,导致不必要的CPU资源消耗,耗电更快。

requestAnimationFrame实现动画可以解决上面两个问题。

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

使用:

接受一个回调,在下次渲染前执行

该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻,这个时间用来进行动画参数的计算。

参考官方示例

  1. const element = document.getElementById('some-element-you-want-to-animate');
  2. let start;
  3. function step(timestamp) {
  4. if (start === undefined)
  5. start = timestamp;
  6. const elapsed = timestamp - start;
  7. //这里使用`Math.min()`确保元素刚好停在200px的位置。
  8. element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
  9. if (elapsed < 2000) { // 在两秒后停止动画
  10. window.requestAnimationFrame(step);
  11. }
  12. }
  13. window.requestAnimationFrame(step);

由于该方法只是通知浏览器下一次绘制之前执行一次callback,因此实现动画时候需要每次重新调用。

requestAnimationFrame返回一个id,取消回调的方法是cancelAnimationFrame,请看官方示例

  1. var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame ||
  2. window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
  3. var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
  4. var start = window.mozAnimationStartTime; // 只有Firefox支持mozAnimationStartTime属性,其他浏览器可以使用Date.now()来替代.
  5. var myReq;
  6. function step(timestamp) {
  7. var progress = timestamp - start;
  8. d.style.left = Math.min(progress/10, 200) + "px";
  9. if (progress < 2000) {
  10. myReq = requestAnimationFrame(step);
  11. }
  12. }
  13. myReq = requestAnimationFrame(step);
  14. window.cancelAnimationFrame(myReq);

需要注意动画中元素样式最好根据时间进行计算,而不是使用需要样式计算的DOM API(例如ele.style.offsetWidth),否则会强制重新渲染。

requestIdleCallback

接受一个回调,回调在浏览器空闲时间执行。注意如果浏览器一直渲染没有空闲,可能就一直执行不到requestIdelCallback注册的回调,因此可以设置一个超时时间,超时之后会执行注册的回调。

取消回调的方法是 cancelIdleCallback

  1. var handle = window.requestIdleCallback(callback, {timeout: 1000});
  2. cancelIdleCallback(handle);

注意,requestIdleCallback用来执行优先级低的任务,由于其执行时机不可控,因此尽量不要执行DOM操作。