你不知道的requestAnimationFrame & requestIdleCallback

855 阅读4分钟

浏览器渲染帧

主流的PC屏幕刷新率(FPS)大多在60Hz,即1秒钟对屏幕进行60次刷新,平均每次刷新耗时大概是16.6ms。

  • 刷新率高于60帧/s,会做一些无用的刷新,浪费cpu资源;
  • 刷新率低于60帧/s(即用户更新页面,页面内容并未及时修改),会出现丢帧导致页面卡顿,用户体验很差。 那么,在浏览器的进行一次屏幕刷新过程中需要做哪些事情呢?我们借助一张浏览器渲染帧流程图来说明

image.png

  1. 新的一帧开始,首先处理用户输入事件,触发相关event的回调(包括但不限于:touch event、input event、wheel event、click event);
  2. 查找定时器Timer任务列表,如果定时任务时间到了,执行定时任务;
  3. 执行上一次渲染帧中注册的 requestAnimationFrame 回调(本次渲染帧注册的requestAnimationFrame 回调在下一次渲染帧才会被执行)
  4. Parse HTML,如果有DOM变动,那么会有解析DOM的这一过程
  5. Recalc Styles,如果你在JS执行过程中修改了样式或者改动了DOM,那么便会执行这一步,重新计算指定元素及其子元素的样式
  6. Layout,如果有涉及元素位置信息的DOM改动或者样式改动,那么浏览器会重新计算所有元素的位置、尺寸信息。而单纯修改color、background等等则不会触发重排
  7. Update layer tree,更新Render Layer的层叠排序关系
  8. Paint,计算得出更新图层的绘制指令
  9. Composite,把绘制指令传到合成线程
  10. 此时如果主线程(Main Thread)在下一帧到来之前还有时间的话,会执行requestIdleCallback回调
  11. 合成线程会安排栅格化操作并通知GPU进程刷新这一帧

由此可见,一次屏幕自动刷新流程和页面渲染也差不多

requestAnimationFrame

上面我们提到过requestAnimationFrame,这个api向浏览器注册回调函数,并在每次重排和重绘之前执行该回调,一般用于优化页面性能

const element = document.getElementById('some-element-you-want-to-animate');
let start;

function step(timestamp) {
  if (start === undefined)
    start = timestamp;
  const elapsed = timestamp - start;

  //这里使用`Math.min()`确保元素刚好停在200px的位置。
  element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';

  if (elapsed < 2000) { // 在两秒后停止动画
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()

requestIdleCallback

通过上面的frame执行周期我们可以看到,当浏览器在一个渲染帧(刷新周期)内完成页面渲染操作后,还有剩余时间,会执行 requestIdleCallback 注册的优先级较低的代码。如:

const sleep = (delay) => {
  const start = Date.now()
  while (Date.now() - start < delay) {}
}
const tasks = [
  () => {
    console.log('第一个任务开始')
    sleep(15)
    console.log('第一个任务结束')
  },
  () => {
    console.log('第二个任务开始')
    sleep(5)
    console.log('第二个任务结束')
  },
  () => {
    console.log('第三个任务开始')
    sleep(20)
    console.log('第三个任务结束')
  },
  () => {
    console.log('第四个任务开始')
    sleep(10)
    console.log('第四个任务结束')
  },
]
const start = Date.now()
const worker = (deadline) => {
  while (
    (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
    tasks.length > 0
  ) {
    tasks.shift()()
    console.log(Date.now() - start)
    if (deadline.timeRemaining()) {
      console.log('继续执行')
    } else {
      console.log('时间不够了,下次执行')
    }
  }
  if (tasks.length) {
    window.requestIdleCallback(worker, { timeout: 1000 })
  }
}

window.requestIdleCallback(worker, { timeout: 1000 })
  • sleep是一个简单的睡眠函数,用来模拟超时任务
  • tasks是我们需要执行的一个任务列表,包含4个任务;
  • worker是我们要执行的低优先级代码;

最后我们调用 requestIdleCallback 注册低优先级代码,在渲染帧内(16.6ms)主线程空闲时去执行该代码块。requestIdleCallback接收两个参数:

  • callback:注册的低优先级callback,该函数的入参是一个deadline对象,包含一个timeRemaining函数用来判断当前渲染帧内是否还有空闲时间;didTimeout判断定时器时间是否到了
  • options:通常我们需要给低优先级代码指定一个定时器,表示如果定时器时间到了,不管主线程是否空闲,都要强制执行,如{ timeout: 1000 }

上面代码依次输入:

image.png

总结

  • requestAnimationFrame 和 requestIdleCallback 分别在页面重排重绘一前一后执行;
  • requestAnimationFrame在每次重绘前一定会执行,requestIdleCallback并不是一定会执行