这是来自 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 开发人员。
14 条评论