使用 mozRequestAnimationFrame 优化 JavaScript 动画

这是来自 Robert O’Callahan 博客 的转载文章。

<b>mozRequestAnimationFrame</b> 是一个实验性 API,用于使 JavaScript 动画更有效率。我们不保证永远支持它,并且我不建议将网站依赖于它。我们已经实现了它,以便人们可以尝试使用它,我们也可以收集反馈。同时,我们将提出它作为标准(显然,减去 moz 前缀),并且作者对我们实现的反馈将有助于我们制定更好的标准。

此功能将在 Firefox 4 Beta 4 中提供。

在 Firefox 4 中,我们添加了对两种主要的声明式动画标准的支持 - SVG 动画(又名 SMIL)和 CSS 过渡。但是,我也强烈认为 Web 需要更好地支持基于 JS 的动画。无论我们如何丰富声明式动画,有时你仍然需要编写 JS 代码来计算(采样)每个动画帧的状态。此外,Web 上已经存在大量的 JS 动画代码,并且如果能够提高其性能和流畅度而无需作者将其重写为声明式形式,那就太好了。

显然,你今天可以使用 setTimeout/setInterval 在 JS 中实现动画,以触发动画采样并调用 Date.now() 来跟踪动画进度。这种方法有两个主要问题。最大的问题是没有“正确”的超时值可供使用。理想情况下,动画应该像浏览器能够重绘屏幕一样频繁地采样,直到某个最大限制(例如,屏幕刷新率)。但是,作者不知道该帧速率将是多少,并且当然它甚至会因时而异。在某些情况下(例如,动画不可见),动画应该完全停止采样。次要问题是,当有多个动画正在运行时 - 一些在 JS 中,一些是声明式动画 - 很难使它们保持同步。例如,你希望脚本能够以相同的持续时间启动 CSS 过渡和 JS 动画,并且就动画被认为已启动的确切时间点达成一致。在每次绘制时,你还要希望它们使用相同的“当前时间”进行采样。

这些问题不时地在邮件列表中出现,例如在 public-webapps 上。一段时间前,我制定了 一个 API 提案,Boris Zbarsky 刚刚实现了它;它在 Firefox 4 beta 4 中。以下是 API,非常简单

  • window.mozRequestAnimationFrame():表示正在进行动画,请求浏览器为下一动画帧安排窗口重绘,并请求在该重绘之前触发MozBeforePaint事件。
  • 浏览器在重绘窗口之前触发MozBeforePaint事件。该timeStamp事件的属性是自纪元开始以毫秒为单位的时间,被认为是本次重绘所有动画的“当前时间”。
  • 还有一个window.mozAnimationStartTime属性,也是自纪元开始以毫秒为单位的时间。当脚本启动动画时,此属性指示该动画应被认为何时启动。这与 Date.now() 不同,因为我们确保在窗口的任何两次重绘之间,window.mozAnimationStartTime 的值保持不变,因此在同一帧内启动的所有动画都获得相同的启动时间。在此时间间隔内触发的 CSS 过渡和 SMIL 动画也使用该启动时间。(在 beta 4 中,有一个错误意味着我们无法完全做到这一点,但我们会修复它。)

就这样!这里有一个 示例;相关的示例代码

var start = window.mozAnimationStartTime;
function step(event) {
  var progress = event.timeStamp - start;
  d.style.left = Math.min(progress/10, 200) + "px";
  if (progress < 2000) {
    window.mozRequestAnimationFrame();
  } else {
    window.removeEventListener("MozBeforePaint", step, false);
  }
}
window.addEventListener("MozBeforePaint", step, false);
window.mozRequestAnimationFrame();

它与通常的 setTimeout/Date.now() 实现并没有太大区别。我们使用 window.mozAnimationStartTime 和 event.timeStamp 来代替调用 Date.now()。我们调用 window.mozRequestAnimationFrame() 来代替 setTimeout()。转换现有代码通常很容易。你甚至可以使用包装器抽象化差异,该包装器在 mozAnimationStartTime/mozRequestAnimationFrame 不可用时调用 setTimeout/Date.now。当然,我们希望这成为标准,因此最终不再需要此类包装器!

即使在这个简单的情况下,使用此 API 也有几个优点。作者不必猜测超时值。如果浏览器超载,动画将优雅地降级,而不是无用地比必要次数更多地运行步骤脚本。如果页面处于隐藏选项卡中,我们将能够将帧速率降低到非常低的值(例如,每秒一帧),从而节省 CPU 负载。(此功能尚未发布。)

此 API 的一个重要功能是 mozRequestAnimationFrame 是“一次性”的。如果动画仍在运行,你必须从事件处理程序中再次调用它。另一种选择是拥有“beginAnimation”/“endAnimation” API,但这似乎更加复杂,并且在错误情况下略微更有可能导致动画永远运行(浪费 CPU 时间)。

此 API 与将某些声明式动画卸载到专用“合成线程”的浏览器实现兼容,以便即使主线程被阻塞,它们也能进行动画。(Safari 做到了这一点,我们也在构建类似的东西。)如果主线程在一个事件上被长时间阻塞(例如,如果 MozBeforePaint 处理程序运行时间过长),JS 动画显然不可能与卸载到合成线程的动画保持同步。但是,如果主线程保持响应,因此 MozBeforePaint 事件可以在合成线程执行的每个合成步骤之间分派和处理,我认为我们可以使 JS 动画与卸载的动画保持同步。我们需要仔细选择 mozAnimationStartTime 和 event.timeStamp 返回的动画时间戳,并“足够早”地分派 MozBeforePaint 事件。

编辑:mozRequestAnimationFrame 帧速率限制
(来自 Robert O'Callahan 博客

一些人一直在使用 mozRequestAnimationFrame,并且注意到他们无法获得超过每秒 50 帧的速度。这是故意的,它是一个很好的功能。

在现代系统上,应用程序通常无法在屏幕上获得超过每秒 50-60 帧的速度。造成这种情况的原因有很多。其中一些是硬件限制:CRT 具有固定的刷新率,LCD 在更新屏幕的速度方面也有限制,这是由于 DVI 连接器中的带宽限制和其他原因。另一个主要原因是,现代操作系统倾向于使用“合成窗口管理器”,它们以固定的速率重新绘制整个桌面。因此,即使应用程序每秒更新其窗口 100 次,用户也无法看到超过一半的更新。(某些平台上的某些应用程序,通常是游戏,可以全屏显示,绕过窗口管理器并以硬件允许的最快速度更新到屏幕上,但显然桌面浏览器通常不会这样做。)

因此,每秒触发超过 50 次的 MozBeforePaint 事件只会浪费 CPU(即,电力)。所以我们没有这样做。除了节省电力外,减少动画 CPU 使用量还有助于整体性能,因为我们可以使用空闲时间来执行垃圾回收或其他清理任务,从而减少帧跳跃的发生率或持续时间。

我们需要做一些后续工作以确保在每个平台上使用最佳速率;现代平台具有 API 可以告诉我们窗口管理器的合成速率。但是 50Hz 几乎总是非常接近。

这意味着一旦你达到 50 帧或更多帧,测量 FPS 是一种糟糕的性能衡量方法。此时,你需要提高工作负载的难度。

告诉我们你的想法。

关于 Paul Rouget

Paul 是 Firefox 开发人员。

Paul Rouget 的更多文章…


14 条评论

  1. Luca

    直接在 mozRequestAnimationFrame 中注册回调(步骤)怎么样?

    2010 年 8 月 16 日 下午 6:25

  2. Robert O’Callahan

    这不是一个坏主意…

    2010 年 8 月 17 日 上午 2:31

  3. Julián Ceballos

    这对浏览器来说是个好主意,它可以使 JavaScript 的图形动画变得更好,并且不会强迫浏览器仅仅通过更改图像或像素值来模拟动画。

    2010 年 8 月 21 日 下午 10:49

  4. Daniel Cassidy

    如果我理解正确,这就是 JavaScript 的 vsync()。如果是这样,那么坦率地说,有人实现它真是太好了。我简直不敢相信当发现 canvas 无法与刷新率同步时。

    但是 - 你是在说你将帧速率硬性限制为 50Hz 吗?如果是这样,那真是太疯狂了。我从未听说过显示器以低于 60Hz 的速度刷新,因此你任意地强迫 JS 动画跳过至少 1/6 的所有帧,这会导致动画不必要的抖动。

    另一方面,如果你只是说 MozBeforePaint 不会比刷新率更频繁地触发,那么是的,显然这是正确的事情。我感到困惑的是,为什么有人会抱怨这一点。

    2010 年 8 月 26 日 下午 2:46

  5. how.,e

    我认为我们应该保留使用类似于时间线动画数据结构的 tween 类型类... IE 在动画序列中添加关键帧,而不是调用使用回调来运行下一个关键帧/动画的函数

    例如... AddAnimation({duration:5, x:5},{duration:10, x:20});
    这基本上添加了两个关键帧。

    我认为通过这种方法,人们可以用更传统的方式构建动画工具,这可以开辟更多可能性。

    2010 年 9 月 4 日 下午 5:11

  6. louis-rémi

    如何使用一种与 setInterval 不同的方法,该方法始终在重绘之前触发回调,因此不需要任何毫秒参数。

    setFrameInterval(function() {
    // 在我绘制之前执行某些操作
    });

    我在 jQuery 的动画部分工作过,这似乎更容易利用。

    2010 年 10 月 21 日 上午 11:32

  7. Evgeny

    在我的 IE 画布实现中,我使用了这个技巧
    canvas.onframe = function(){
    // … 绘制例程
    }
    因此动画与闪烁帧速率同步。

    2010 年 11 月 7 日 下午 01:54

  8. Reyboz 博客

    Che dire.. Complimenti per il progetto!

    2010 年 12 月 13 日 下午 12:42

  9. Victor

    我认为,window.mozAnimationStartTime 应该用函数替换

    window.mozAnimationStartTime(),这样我们就可以为这个 API 创建 Javascript polyfil,并在旧浏览器中使用它…
    (使用属性我们不能这样做,因为 getter 在某些浏览器中不受支持)

    2011 年 1 月 16 日 下午 23:44

  10. Jorge

    “…但 50Hz 几乎总是非常接近。”让我想起了“640K 的内存对任何人都应该足够了”。

    然后大多数新款电视机正在做 120Hz。
    我不喜欢固定数字,没有理由这样做。根据硬件进行节流或将其设置为一个选项,以 50 作为默认值会更好,也更具前瞻性。

    2011 年 3 月 24 日 上午 11:34

  11. Joe

    在 FireFox 3.6 或 4 中,我发现将你的绘制代码附加到 'mozBeforePaint' 会使你的代码更新速度略快,而不是将其传递给 'requestAnimationFrame'。这就像以 60fps 的速度运行,而不是 58fps,所以它很小。

    在 Windows 上的 FireFox 6 中,我发现使用 mozBeforePaint 会导致性能下降 30%!从 60fps 降低到 40fps。

    所以,我建议将你的函数传递给 'requestAnimationFrame',而不是使用 'mozBeforePaint'。

    2011 年 8 月 26 日 下午 16:59

  12. milo

    我制作了一个 Menucool jQuery 滑块 (www.menucool.com/slider/jquery-slider),它在使用 requestAnimationFrame 后在大多数浏览器中流畅地动画,但在 Firefox 中则不然。即使我将 fms 设置为 50,它在 Firefox 中看起来也笨拙且跳跃,我相信 mozRequestAnimationFrame 的效率不如 webkitRequestAnimationFrame 或 msRequestAnimationFrame。

    我怎样才能让它在 FF 中像在其他浏览器中一样流畅?有人能帮我解决这个问题吗?

    2012 年 6 月 30 日 下午 18:52

  13. milo

    PS:上面提到的 jQuery 滑块实际上使用的是纯 Javascript。它没有使用 jQuery 库,因为 jQuery 自 1.6 版本起就取消了 requestAnimationFrame。

    2012 年 6 月 30 日 下午 18:58

  14. Greg

    我已经从这个想法开始构建了一个完整的游戏,我认为它太棒了,我真的希望它足够受欢迎,让你来支持它!

    2012 年 10 月 20 日 下午 12:36

这篇文章的评论已关闭。