本文深入分析JavaScript动画性能,对比Velocity.js、GSAP与jQuery及CSS转换的优劣。重点讲解如何通过避免布局抖动、使用requestAnimationFrame及缓存DOM值来优化动画,并指出Velocity.js在轻量级与功能丰富性上的平衡优势。
JavaScript动画性能概述
基于JavaScript的动画在速度上甚至能与CSS过渡效果相媲美,这如何实现?Adobe和Google持续发布的富媒体移动网站性能已接近原生应用,这又是如何达成的?
本文将逐一检视基于JavaScript的DOM动画库,如Velocity.js和GSAP,分析其为何在性能上优于jQuery和CSS动画。
jQuery动画的性能局限
首先从基础开始:JavaScript与jQuery常被错误地混为一谈。JavaScript动画本身运行迅速,而jQuery却拖慢了速度。原因在于,尽管jQuery功能强大,但其设计目标从来不是成为高性能的动画引擎:
- jQuery无法避免布局抖动,这归因于其代码库服务于动画之外的多种用途。
- jQuery的内存消耗常触发垃圾回收,导致动画过程中出现间歇性卡顿。
- jQuery使用setInterval而非requestAnimationFrame(RAF),以保护新技术不受其自身影响。
需注意,布局抖动是动画起始阶段不流畅的根源,垃圾回收是动画期间卡顿的元凶,而未使用RAF则导致低帧率。
实现示例
避免造成布局抖动的DOM查询与更新组合:
var currentTop,
currentLeft;
/* With layout thrashing. */
currentTop = element.style.top; /* QUERY */
element.style.top = currentTop + 1; /* UPDATE */
currentLeft = element.style.left; /* QUERY */
element.style.left = currentLeft + 1; /* UPDATE */
/* Without layout thrashing. */
currentTop = element.style.top; /* QUERY */
currentLeft = element.style.left; /* QUERY */
element.style.top = currentTop + 1; /* UPDATE */
element.style.left = currentLeft + 1; /* UPDATE */
在更新之后进行查询会强制浏览器重新计算页面的样式数据(同时纳入新的更新效果)。这会产生显著的动画开销,而这一切仅发生在16毫秒的微小超时间隔内。
类似地,实现RAF并不需要对现有代码库进行大幅重构。让我们比较RAF的基础实现与setInterval:
var startingTop = 0;
/* setInterval: Runs every 16ms to achieve 60fps (1000ms/60 ~= 16ms). */
setInterval(function() {
/* Since this ticks 60 times a second, we divide the top property's increment of 1 unit per 1 second by 60. */
element.style.top = (startingTop += 1/60);
}, 16);
/* requestAnimationFrame: Attempts to run at 60fps based on whether the browser is in an optimal state. */
function tick () {
element.style.top = (startingTop += 1/60);
}
window.requestAnimationFrame(tick);
RAF带来了推动动画性能的最大可能性,你只需对代码进行单一变更。
CSS转换的优劣分析
CSS转换
CSS转换通过将动画逻辑交由浏览器处理,从而超越jQuery,其优势体现在:(1)优化DOM交互与内存消耗以避免卡顿(抖动),(2)利用引擎的RAF机制,(3)强制硬件加速(利用GPU提升动画性能)。
然而现实是,这些优化也可在JavaScript中直接执行。GSAP已这样做多年。Velocity.js这一新动画引擎不仅利用了相同技术,还向前迈出了几步——我们稍后探讨。
直面事实,JavaScript动画能与CSS转换竞争只是我们康复计划的第一步。第二步是实现“JavaScript动画实际上可以比CSS转换更快”。
现在我们开始讨论CSS变换的弱点:
- transition强制硬件加速会增加GPU消耗,高负荷下导致运行不流畅。这种情况在移动设备上尤为明显。(特殊情况下,如数据在浏览器主线程与排版线程间传递产生的瓶颈也会导致不流畅)。某些CSS属性,如transform和opacity,则不受这些瓶颈影响。Adobe在此详细总结了这些问题。
- transition在IE10以下无效,导致自IE8和IE9以来的桌面站点可用性问题至今广泛存在。
- 由于transition并非由JavaScript原生控制(仅由JavaScript触发),浏览器无法获知如何与控制这些transition的JavaScript代码同步优化它们。
相反,基于JavaScript的动画库可自行确定何时开启硬件加速。它们原生支持各版本IE浏览器,且特别适合批量动画优化。
我的建议是:仅当你单独为移动端开发且仅实现简单动画时使用原生CSS变换。在此环境下,transition是一种原生有效的解决方案,使你能在样式表中实现所有动画逻辑,无需添加额外JavaScript库,从而避免页面臃肿。然而,当设计复杂UI或开发具有不同状态的UI应用时,应使用动画库以保持动画流畅,并使工作流程易于管理。Transit是一个在管理CSS变换方面表现优异的库。
JavaScript动画库对比
JavaScript动画
好了,JavaScript在性能方面占据上风。但JavaScript具体快了多少?最初,构建一个实际的3D动画示例已足够快,通常此类场景仅使用WebGL。构建一个多媒体小动画也足够,通常仅使用Flash或After Effects构建。构建一个虚拟世界也足够,通常仅使用canvas构建。
为了直接对比领先的动画库(当然包含使用CSS渐变效果的Transit),请回顾Velocity在VelocityJS.org上的文档。
问题仍是:JavaScript如何具体达成其高水平性能?以下是对基于JavaScript动画执行目标的优化简短清单:
- 同步DOM → 在整个动画链中间入栈以最小化布局抖动。
- 为整个链式调用缓存属性值,以最小化DOM查询发生(这些是高性能DOM动画的陷阱)。
- 在相同调用中缓存整个同级别元素的单元转换率(如px到%,em等)。
- 当更新可能在视觉上不可见时跳过样式更新。
回顾我们先前关于布局抖动的知识,Velocity.js利用这些最佳实践来缓存动画结束值,以便复用为后续动画的开始值,从而避免重新查询DOM以获取元素的开始值:
$element
/* Slide the element down into view. */
.velocity({ opacity: 1, top: "50%" })
/* After a delay of 1000ms, slide the element out of view. */
.velocity({ opacity: 0, top: "-50%" }, { delay: 1000 });
在上述例子中,第二个Velocity调用知道它应自动从opacity为1和top为50%开始。
浏览器本身最终能执行许多相同的优化,但这样做会显著减少开发者编写动画代码的方式。因此,出于同样原因,由于jQuery不使用RAF(如上所述),浏览器不会强制优化它,甚至给出微小的机会去打破规格或偏离预期行为。
最后,我们对这两个JavaScript动画库(Velocity.js和GSAP)进行互相比较。
GSAP的优缺点
GSAP是首个用于演示JavaScript DOM令人印象深刻的动画表现的动画库。它确实如此,但存在一些缺点:
- 在中到高负荷动画中,GSAP的DOM交互开销导致动画在开始和过程中失帧。
- 与Velocity.js在超宽松的MIT许可下发布不同,GSAP是闭源的,且在许多类商用场景下需要许可年费。
- 因为GSAP是一个完整的动画套件,体积是Velocity的三倍。然而,GSAP功能丰富,堪称动画界的瑞士军刀。
我推荐的做法是:在需要精确控制定时(如重绘、暂停/恢复)和运动(如贝塞尔曲线路径)时使用GSAP。这些特性在游戏开发和某些特殊应用中至关重要,但通常无需用于网页应用的UI中。
Velocity.js的优势
Velocity.js
引用GSAP的丰富特性并不代表Velocity自身在特性上是轻量级的。相反,在压缩后仅7kb中,Velocity不仅复制了jQuery $.animate()的所有功能,还将颜色动画、转换、循环、easing效果、类动画以及滚动打包了进去。
总之,Velocity是jQuery、jQuery UI以及CSS渐变效果的最佳组合。
此外,从便利角度看,Velocity在内部使用jQuery的$.queue()方法,从而实现与jQuery的$.animate()、$.fade()和$.delay()函数的无缝互操作。而且,由于Velocity的语法与$.animate()的语法相同,你无需改变页面的任何代码。
让我们快速浏览一下Velocity.js。在基础层面,Velocity的行为与$.animate()相同:
$element
.delay(1000)
/* Use Velocity to animate the element's top property over a duration of 2000ms. */
.velocity({ top: "50%" }, 2000)
/* Use a standard jQuery method to fade the element out once Velocity is done animating top. */
.fadeOut(1000);
在其最高级层面,可以创建带有3D动画的复杂滚动场景——几乎只需两行简单代码:
$element
/* Scroll the browser to the top of this element over a duration of 1000ms. */
.velocity("scroll", 1000)
/* Then rotate the element around its Y axis by 360 degrees. */
.velocity({ rotateY: "360deg" }, 1000);