requestAnimationFrame 性能更好

原文:Better performance with requestAnimationFrame
作者:Luz Caballero
译者:涂鸦码龙

介绍

本文讨论如何使用 requestAnimationFrame API 代替旧的 setInterval / setTimeout 方法,从而提高动画的性能。当然,我们会演示代码实例

当前几乎所有的主流浏览器都支持 requestAnimationFrame ,尽管有的还带前缀。Erik Möller 已经写了个 polyfill ,加上这个应该可以支持所有浏览器啦。一言难尽,让我们从头说来…

老方法不太好

在表彰 requestAnimationFrame 之前,我们先看看用老方法是怎么实现动画的。我确定没必要提很久以前 Mozilla 第一个实现了 mozRequestAnimationFrame ,你可能已经用 setTimeoutsetInterval 创建了动画。假定你已经熟悉这两个方法,我不再赘述。如果你想了解更多,可以看 John Resig 的精彩文章,深度解析 JavaScript 计时器如何工作

我并不是轻视这些老方法,它们的确有一些缺点。首先,当相应的浏览器窗口最小化,JavaScript 计时器在背景标签仍然持续运行。因此,浏览器继续运行看不见的动画,导致不必要的 CPU 和电池寿命的消耗。在移动设备尤其严重。

其次,不仅计时器持续运行看不见的动画,而且当时间到了,仍会排队执行它们的回调函数。让我解释下为什么这样会出问题 —— 比如说由于某些原因,回调函数占用太多时间,比你设定的时间要长。一旦计时器时间到了,它将排队执行 “下一次” 回调函数,甚至前一个还没执行完。这一过程不断重复,很快排队了几乎无数的计时器代码,导致浏览器不堪重负。图1 说明这一情况。

图 1:假如你的回调函数执行时间比设定时间更长,多个回调函数排队会阻塞浏览器。

假如你的回调函数执行时间没比设定时间长,setTimeoutsetInterval 仍不是最理想的。两者只能以固定的频率重绘动画,为了让动画更平滑,我们谨慎起见,选择比屏幕刷新率略高的频率。这样导致不必要的绘制,在屏幕刷新率准备绘制动画结果之前,一些帧已经画过了,因此它们被丢弃了。图 2 说明此问题。

图 2:跳帧会导致较高的 CPU 占用和电池消耗,有时甚至导致动画不平稳。

当用这些方法实现循环动画时,缺点更加突出,比如用于游戏或疯狂的实验(像我的 hipster dog),这种情况,循环的动画保证不间断地排队执行新的回调函数。如果你想了解更多关于循环动画的历史,以及使用 setTimeoutsetInterval 时,循环动画表现如何;以及 requestAnimationFrame 如何改变我们的编码形式,我强烈建议阅读 Nicholas Zakas 写的“用 requestAnimationFrame 实现更好的 JavaScript 动画 ”,他的见解深刻。

介绍 requestAnimationFrame

requestAnimationFrame API 的确如你所愿:它把绘制动画的任务直接交给浏览器,浏览器可以做的更好。

requestAnimationFrame 是 W3C 基于脚本动画的定时控制 API 的一部分。

requestAnimationFrame 做了什么

浏览器了解标签和窗口的状态,页面哪部分可见或不可见,了解当浏览器准备绘制时,其它的动画也在运行。requestAnimationFrame 让浏览器负责,允许它使用这些信息优化动画的调度,解决了我们先前讨论的 JavaScript 计时器问题。requestAnimationFrame 的流程像这样:

  • 首先,它仅仅绘制用户可见的动画。这意味着没把 CPU 或电池寿命浪费在绘制处于背景标签,最小化窗口,或者页面隐藏区域的动画上。
  • 第二,当浏览器准备好绘制时(空闲时),才绘制一帧,此时没有等待中的帧。意味着用 requestAnimationFrame 绘制动画不可能出现多个排队的回调函数,或者阻塞浏览器。
  • 第三,由于浏览器准备好时(空闲时)才绘制帧,不会有等待绘制的帧,没有多余的帧绘制。因此动画更平滑,CPU 和电池使用被进一步优化。

我只是说直到当前的帧绘制完成,都没有额外的回调函数在排队。如果你多次调用 requestAnimationFrame() ,每次调用都有回调函数在排队。

另外,浏览器可以把同一页面的多处动画,保持在单一的回流和重绘周期里。

requestAnimationFrame 没做什么

  • 创建一个连续的动画;它仅安排单独的更新,通过一个返回的 id 号识别。如果还有后续的动画帧,requestAnimationFrame 将在回调函数里再次被调用。如果需要停止动画,使用 cancelAnimationFrame(id)
  • 确保真正需要时再绘制。
  • 保证动画的同步性。如果你同时开始两个动画,但是一个在可见区域,另一个不是,第一个动画将执行,另一个则不会;当第二个变得可见时,它们也许不同步了。如果你关心这个,在写动画代码的时候要注意, 指定一个参数确保所有需要同步的动画状态,不受可见程度的影响(如一组动画从开始以来经过的时间),而不是根据每个动画的前一帧。
  • 即使你尝试用任何方法,触发回调中间的回流(像 getComputedStyle() ,正常情况下,可以触发回流和重绘),它也会等到回调函数执行完毕才绘制。

如何使用 requestAnimationFrame

主流浏览器都支持 requestAnimationFrame ,但是有的仍需前缀。写本文的时候,加前缀的情况如下:

  • Opera: Opera 15 以后无前缀
  • Chrome: Chrome 24+ 无前缀
  • Safari: 有前缀
  • Firefox: 有前缀,Firefox 23+ 无前缀
  • IE: IE 10 以后无前缀

为了确保你的代码万无一失,应该使用 Erik Möller’s polyfill (它提供了健全的跨浏览器支持),这是在 Paul Irish 的这篇文章 的代码基础上改进而来的。

这是我们的青蛙实例中处理动画的简单代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var requestId = 0;
var animationStartTime = 0;
function animate(time) {
var frog = document.getElementById("animated");
frog.style.left = (50 + (time - animationStartTime)/10 % 300) + "px";
frog.style.top = (185 - 10 * ((time - animationStartTime)/100 % 10) + ((time - animationStartTime)/100 % 10) * ((time - animationStartTime)/100 % 10) ) + "px";
var t = (time - animationStartTime)/10 % 100;
frog.style.backgroundPosition = - Math.floor(t / (100/2)) * 60+ "px";
requestId = window.requestAnimationFrame(animate);
}
function start() {
animationStartTime = window.performance.now();
requestId = window.requestAnimationFrame(animate);
}
function stop() {
if (requestId)
window.cancelAnimationFrame(requestId);
requestId = 0;
}

requestAnimationFrame 方法告诉浏览器,基于脚本的动画需要重新取样,需要往 animation frame request callback list(动画帧请求回调列表)插入一个 callback (回调函数)。这些保存的回调函数的 id 列表正等待执行。调用 requestAnimationFrame 返回 id(requestId),用来识别队列中的回调函数。 id 可以通过 cancelAnimationFrame(id) 取消回调函数。

requestAnimationFrame 方法作为它的参数的回调方法,用来绘制动画(animate)的新帧。当接到动画更新请求(time)时,回调函数收到一个 timestamp (时间戳)参数。

时间戳是调用 Performance 接口的 now 方法 的执行结果(译者注:Performance 支持情况)。你需要确保任何其它想拿来比较的时间测量单位也是 DOMHighResTimeStamp —— 以上例子我们调用 window.performance.now,把它的结果存在 animationStartTime 变量中。在动画函数(animate)中,我们在每一帧都用开始时间跟当前时间(time)做比较,计算每个青蛙实例的位置。

当你写动画时,如果碰到一些旧教程,注意 animationStartTime 属性是内建的,但这里并不赞成,因此你必须像上面那样自己记录开始时间。

要看代码效果,你可以看我的 requestAnimationFrame 实例

总结

本文我们讨论了如何通过 requestAnimationFrame 提高 JavaScript 动画的性能,以及如何在所有浏览器中成功使用它。我希望通过此文激发你去尝试一些很酷的动画实验 —— 你可能已经着手改造旧的动画代码了吧!