对requestAnimationframe的理解

... 2025-7-10 CSS 大约 7 分钟

requestAnimationframe ,HTML5 新增的 api,类似于 setTimeout 定时器。

window 对象的一个方法 window.requestAnimationFrame ,浏览器(所以只能在浏览器中使用)专门为动画提供 API,让 dom 动画 canvas 动画、svg 动画、webGL 动画等有一个统一的刷新机制。

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

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

# 特点

  • 按帧对网页进行重绘。该方法告诉浏览器希望执行动画并请求浏览器在下一次重绘之前调用就掉函数更新动画。

  • 由系统来决定回调函数的执行机制。在运行时浏览器会自动优化方法的调用。

  • 显示器有固定的刷新频率(60Hz 、 75Hz 、90Hz 等),每秒最多只能重绘固定的次数,requestAnimationFrame 的基本思想让页面重绘的频率与这个刷新频率保持同步。比如显示器屏幕刷新率为 60Hz,使用 requestAnimationFrame API,那么回调函数就每 1000ms / 60 ≈ 16.7ms 执行一次;如果显示器屏幕的刷新率为 75Hz,那么回调函数就每 1000ms / 75 ≈ 13.3ms 执行一次。

  • 通过 requestAnimationFrame 调用回调函数引起的页面重绘或回流的时间间隔和显示器的刷新时间间隔相同。所以 requestAnimationFrame 不需要像 setTimeout 那样传递时间间隔,而是浏览器通过系统获取并使用显示器刷新频率。

  • 在隐藏或不可见的元素中, requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量

  • requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销

# 优势

  • setTimeoutsetInterval 的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行

  • requestAnimationFrame 采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果

  • 使用 setTimeout 实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,而且还浪费 CPU 资源。而 requestAnimationFrame 则完全不同,当页面处理未激活的状态下,该页面的屏幕绘制任务也会被系统暂停,因此跟着系统步伐走的 requestAnimationFrame 也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了 CPU 开销。

  • 在高频率事件(resize,scroll 等)中,为了防止在一个刷新间隔内发生多次函数执行,使用 requestAnimationFrame 可保证每个绘制间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个绘制间隔内函数执行多次时没有意义的,因为显示器每 16.7ms 绘制一次,多次绘制并不会在屏幕上体现出来。

# 使用

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。回调函数执行次数通常是每秒 60 次,但在大多数遵循 W3C 建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当 requestAnimationFrame() 运行在后台标签页或者隐藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。

回调函数会被传入 DOMHighResTimeStamp 参数,DOMHighResTimeStamp 指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为 1ms(1000μs)。

# 语法
window.requestAnimationFrame(callback)
1

# 参数

callback

下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入 DOMHighResTimeStamp 参数,该参数与 performance.now()的返回值相同,它表示 requestAnimationFrame() 开始去执行回调函数的时刻。

# 返回值

一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。

# 优雅降级

由于 rAF 目前还存在兼容性问题,而且不同的浏览器还需要带不同的前缀。因此需要通过优雅降级的方式对 rAF 进行封装,优先使用高级特性,然后再根据不同浏览器的情况进行回退,直止只能使用 setTimeout 的情况,因此可以这么写:

window.requestAnimFrame = (function () {
  return (
    window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function (callback) {
      window.setTimeout(callback, 1000 / 60)
    }
  )
})()
1
2
3
4
5
6
7
8
9
10

但这种写法没有考虑 cancelAnimationFrame 的兼容性,并且不是所有的设备绘制时间间隔都是 1000/60,下面的代码是比较全的一个 polyfill:

if (!Date.now)
  Date.now = function () {
    return new Date().getTime()
  }
;(function () {
  'use strict'

  var vendors = ['webkit', 'moz']
  for (var i = 0; i < vendors.length && !window.requestAnimationFrame; ++i) {
    var vp = vendors[i]
    window.requestAnimationFrame = window[vp + 'RequestAnimationFrame']
    window.cancelAnimationFrame =
      window[vp + 'CancelAnimationFrame'] ||
      window[vp + 'CancelRequestAnimationFrame']
  }
  if (
    /iP(ad|hone|od).*OS 6/.test(window.navigator.userAgent) || // iOS6 is buggy
    !window.requestAnimationFrame ||
    !window.cancelAnimationFrame
  ) {
    var lastTime = 0
    window.requestAnimationFrame = function (callback) {
      var now = Date.now()
      var nextTime = Math.max(lastTime + 16, now)
      return setTimeout(function () {
        callback((lastTime = nextTime))
      }, nextTime - now)
    }
    window.cancelAnimationFrame = clearTimeout
  }
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# setTimeout

setTimeout 其实就是通过设置一个间隔时间来不断的改变图像的位置,从而达到动画效果的。

但我们会发现,利用 seTimeout 实现的动画在某些低端机上会出现卡顿、抖动的现象。 这种现象的产生有两个原因:

  1. setTimeout 的执行时间并不是确定的。在 JavaScript 中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,所以 setTimeout 的实际执行时机一般要比其设定的时间晚一些。

  2. 刷新频率受 屏幕分辨率 和 屏幕尺寸 的影响,不同设备的屏幕绘制频率可能会不同,而 setTimeout 只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

以上两种情况都会导致 setTimeout 的执行步调和屏幕的刷新步调不一致,从而引起丢帧现象。

setTimeout 的执行只是在内存中对元素属性进行改变,这个变化必须要等到屏幕下次绘制时才会被更新到屏幕上。

如果两者的步调不一致,就可能会导致中间某一帧的操作被跨越过去,而直接更新下一帧的元素。

# 浏览器 UI 线程

浏览器让执行 JavaScript 和更新用户界面(包括重绘和回流)共用同一个单线程,称为“浏览器 UI 线程”

浏览器 UI 线程的工作基于一个简单的队列系统,任务会被保存到队列中直到进程空闲。一旦空闲,队列中的下一个任务就被重新提取出来并运行。这些任务要么是运行 JavaScript 代码,要么执行 UI 更新。

# 参考

window.requestAnimationFrame | MDN (opens new window)

requestAnimationFrame (opens new window)

requestAnimationFrame 知多少 (opens new window)

被誉为神器的 requestAnimationFrame (opens new window)

上次编辑于: 2025年7月10日 04:01
贡献者: HugStars