requestAnimationFrame与定时器性能对比

2,547 阅读3分钟

image.png requestAnimationFrame动画性能好在哪?这是我6年前面试腾讯的一道面试题,上面ChatGPT的回答已经解释的挺清楚了,拿来应付面试基本足够,但是有些细节比较模糊,比如第二行能够在合适的时间内重复执行动画帧,提升渲染性能,什么是合适的时间,与定时器的触发时机有什么不同。定时器(setTimeoutsetInterval)是在指定的时间后调用执行,而requestAnimationFrame会在下一帧渲染之前调用,可以理解为是监听帧变化事件,通过性能面板可以观察出requestAnimationFrame与定时器的区别。

requestAnimationFrame

image.png

定时器

image.png 可以发现两者区别在于回调触发页面渲染之间的间隔,requestAnimationFrame帧变化是紧密联系的,回调触发与页面渲染之间是几乎是连续执行的,而定时器与页面渲染之间会有间隔,这样子带来的性能问题是什么?理论上定时器也可以设置每17ms执行一次,这样也能保证1s内接近60次渲染,从而确保动画流畅,为了验证两者的区别,使用requestAnimationFrame和定时器setInterval分别实现水平移动的动画效果。 0.gif 上面的绿色长条是requestAnimationFrame动画,下面的红色长条是setInterval实现的定时器动画,仔细观察,定时器动画在移动的过程中会有一点抖动,requestAnimationFrame动画移动的过程相对比较平稳,而定时器动画肉眼出现抖动,说明1s内渲染次数小于60次,也就是帧与帧之间大于17ms,为了进一步验证,我们在代码中记录了动画耗时卡顿次数最长卡顿间隔,这三个指标数值越大说明动画性能越差。

// 开始动画按钮的点击事件
function start() {
  window.startTime = Date.now()
  animationByRaf()
  animationBySetInterval()
}
// requestAnimationFrame动画
function animationByRaf(val = 1, now = Date.now(), times = 0, maxPeriod = 0){
  requestAnimationFrame(() => {
    if(Date.now() - now > 20) {
      // 帧间隔时长大于20ms,认为卡顿
      // 收集卡顿次数
      times++
      // 收集最大卡顿间隔
      maxPeriod = Math.max(maxPeriod, Date.now() - now)
    }
    // 更新动画的开始时间,与下一帧动画的Date.now()相减计算出帧间隔时长
    now = Date.now()

    // 水平移动动画,每帧移动1px
    document.querySelector(`#requestAnimationFrameSelector`).style.left = `${val}px`;

    if(val >= 500) {
      // 移动500px后动画停止,打印日志
      console.log(`requestAnimationFrame动画耗时${Math.floor(Date.now() - startTime)},卡顿次数${times},最长卡顿${maxPeriod}ms`)
      return
    }
    // 递归调用实现动画
    animationByRaf(val+1, now, times, maxPeriod)
  })
}

// setInterval动画
function animationBySetInterval(){
  let val = 1
  let now = Date.now()
  let times = 0
  let maxPeriod =0
  const interval = setInterval(() => {
    if(Date.now() - now > 20) {
      // 帧间隔大于20ms,认为卡顿
      times++
      maxPeriod = Math.max(maxPeriod, Date.now() - now)
    }
    now = Date.now()

    document.querySelector(`#setIntervalSelector`).style.left = `${val++}px`

    if(val > 500) {
      // 动画结束,打印日志
      console.log(`setInterval动画耗时${Date.now() - startTime},卡顿次数${times},最长卡顿${maxPeriod}ms`)
      clearInterval(interval)
      return
    }
  }, 17)
}

image.png 上图是动画结束后的日志,执行500次动画,requestAnimationFrame只有一次卡顿,而setInterval却有53次,理论上setInterval动画应该在17*500=8350ms的时候结束,但实际却耗时8502ms,延迟了172ms

image.png

通过性能面板也能发现setInterval某些帧的触发时机并不稳定,导致动画卡顿,原因就是定时器设置的延迟时间,不是回调函数执行的延迟时间,而是加入事件队列的延迟时间,加入事件队列后需要等待主线程空闲,再将事件队列中的任务加入执行栈执行,由于定时器不能稳定触发,所以无法保证每一帧能够准时渲染,而requestAnimationFrame帧变化紧密联系,是在每一帧渲染前触发,所以能够在合适的时间内重复执行动画帧,提升渲染性能