用requestAnimationFrame 来让JS动画更流畅

浏览量:433

前言

都 2022 年了,还不在页面中加点动画吗?

合理高效的动画会让用户感觉的体验与交互更好,在 web 中,常常使用 CSS 动画(transition 与 keyFrames)、SVG 动画(SMIL, JavaScript, CSS)、JavaScript 来实现动画。

其中,JavaScript 因为可控制的细节更多,可完成的效果也更多。

按照旧有的方式,通常在使用 JavaScript 做动画时,会使用 setTimeout/setInterval 来完成动画效果,比如旧版本的 jQuery 中的 animate 方法(新版本的已经使用requestAnimationFrame)。

但是,都 2022 年了,放过 setTimeout/setInterval 吧,它们真的不适合做这个。

requestAnimationFrame 这个 API,看名字就知道是为了动画而生的,但它作为Web API,只要需求符合,一样可以利用其特性完成效果。

让我们一起来了解一下。

动画帧

无论用什么来实现动画,运用的原理都是相近的。

人眼有短暂的视觉残留,即当人眼看到一个图像信息后,这个画面会短暂的停留在视网膜上一段时间。

早期电影或动画行业常用的就是 24 帧,即 1s 播放 24 个一连串的画面,每一个画面就是 1 帧。但实际来说,帧率(FPS)越高,实际感觉就会越流畅。

在 web 上并没有什么不同,web 动起来所要的帧(画面)也是相近的,但在 web 上通常不会选择 24 作为帧率,而是采用浏览器的刷新率来作为帧率。

通常情况下这一值是 60 帧,即我们需要在 1000/60≈16.7ms 内完成一帧的绘制。

为什么不要用 setTimeout/setInterval 来完成动画

有了时间,有了要做的目标(绘制新画面,通常来说就是操作元素),很容易就会想到使用setTimeout/setInterval API 来做。

但最大最大的问题是,setTimeout/setInterval 的 delay 值并不精确,它并不能保证代码在指定的时间去执行代码,MDN 甚至直接将其写进了参数说明:

不管是哪种情况,实际的延迟时间可能会比期待的(delay 毫秒数) 值长

这对于动画来说是致命的,肉眼很容易感知出这种“不流畅”。

具体的原因需要了解浏览器的 Event-loop 机制与 DelayTask,但不管怎样,它们执行的时机都是不准确的。

那么把 delay 值改小一些可以吗?

当然也是不行的,例如设为 10ms 更新画面,这样在实际显示时必定会忽略某画面,造成跳帧(某画面肉眼并没有看到)。

那么有没有可以准确执行更新画面的 API 呢?当当当当当,主角 requestAnimationFrame 出马了。

为什么用 requestAnimationFrame 更好

与刷新率吻合的时间

setTimeout/setInterval 的时间不准确,而 requestAnimationFrame 的时间可以说非常准确。

它执行的时机完全取决于浏览器重绘的时机,它的回调函数会在下次浏览器重绘之前执行。

大部分的浏览器刷新率都是 60hz, 但近些年来高刷(75hz, 90hz, 120hz, 144hz, 240hz)显示设备在快速普及,requestAnimationFrame 会根据显示设备的刷新率来运行,可以看如下测试:

注意 requestAnimationFrame 的回调中会有一个参数,它是一个DOMHighResTimeStamp 类型值,所以时间差可以到小数位。

当 startLog 时,实测在 60hz 的显示设备下,log 的值通常是 16.683 左右,在 120hz 的显示设备下,log 的值通常为 8.3 左右,时间值基本都与刷新率所吻合。

避免无效的资源浪费

requestAnimationFrame 运行在重绘之前,所以它会将每一帧的 DOM 操作集中起来,在一次中回流或重绘中完成。这意味着即使在不同位置同时触发了多次 requestAnimationFrameTime 回调,更新也只会在一帧中进行。这一点也可以在回调函数的参数中得以验证:

在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。

当页面不是激活状态时,requestAnimationFrame 会自动停止,也会节省资源开销。

requestAnimationFrame 的其他用处

给事件添加节流或防抖

某些事件在浏览器中需要做节流或者防抖,这时使用requestAnimationFrame 替代setTimeout 可以获得更好的性能。
类似下面的防抖函数,

function rafThrottle(func) {
  let lock = false;
  return function (...args) {
    if (lock) return;
    lock = true;
    window.requestAnimationFrame(() => {
      func.apply(this, args);
      lock = false;
    });
  };
}

时间切片(Time Slicing)

JavaScript 作为单线程语言,当运行长任务时会阻塞其他行为,在web 上来看就像是页面假死。

当延迟超过100ms,用户就会察觉到延迟感。

为了避免这种情况,通常会采用Web Worker 或者时间切片(Time Slicing)。

Web Worker 采用了单独开线程而不阻塞用户行为的方式来进行任务,这里不再细说。

而时间切片是采用分割长任务的方式来解决这一问题,限制任务的可执行时间,从而不影响用户交互。

留下评论