深入探究使JavaScript动画流畅的一些方法
基于Javascript的动画暗中同CSS过渡效果一样,甚至更加快,这怎么可能呢?而Adobe和Google持续发布的富媒体移动网站的性能可媲美本地应用,这又怎么可能呢?
本文逐一遍览了基于Javascript的DOM动画库,如Velocity.js和GSAP,看其是如何比jQuery和CSS动画效果更具性能的.
jQuery
让我们先从基础的开始:JavaScript和jQuery被错误的混为一谈了.JavaScript动画是很快的.jQuery把它放慢了下来。为什么?因为—尽管jQuery非常强大—但成为一个性能强劲的动画引擎从来都不是jQuery的设计目标:
- jQuery不能避免布局颠簸,这得归因于它的代码库提供了动画之外的多种用途.
- jQuery的内存消耗经常会触发垃圾回收,那样会时不时的让动画定格下来.
- jQuery使用setInterval而不是requestAnimationFrame(RAF)来保护新技术不受其自身的影响.
应该注意到布局颠簸就是在动画开始部分的不顺畅,垃圾回收就是造成动画期间不顺畅的元凶,而没有使用RAF则会导致低帧率.
实现示例
避免造成布局颠簸的DOM查询和更新组合:
varcurrentTop, currentLeft; /*Withlayoutthrashing.*/ currentTop=element.style.top;/*QUERY*/ element.style.top=currentTop+1;/*UPDATE*/ currentLeft=element.style.left;/*QUERY*/ element.style.left=currentLeft+1;/*UPDATE*/ /*Withoutlayoutthrashing.*/ 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比较一下:
varstartingTop=0; /*setInterval:Runsevery16mstoachieve60fps(1000ms/60~=16ms).*/ setInterval(function(){ /*Sincethisticks60timesasecond,wedividethetopproperty'sincrementof1unitper1secondby60.*/ element.style.top=(startingTop+=1/60); },16); /*requestAnimationFrame:Attemptstorunat60fpsbasedonwhetherthebrowserisinanoptimalstate.*/ functiontick(){ element.style.top=(startingTop+=1/60); } window.requestAnimationFrame(tick);
RAF产生了推动动画性能的最大可能性,你可以对你的代码进行单一的变更.
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的App时,你就应该使用动画库以使动画保持流畅,同时使工作流程易于管理。Transit是一个在管理CSS变换方面做得尤其优秀的库。
JavaScript动画
好了,那JavaScript可就在性能方面占据上风了.但Javascript究竟具体快了多少呢?好吧—最初—对于构建一个实在的3D动画示例是足够快的,通常在构建中你只会看到有使用WebGL.而构建一个多媒体小动画也够了,通常你看到只会使用Flash或者AfterEffects构建.而构建一个虚拟世界也够了,通常你只会看到使用canvas构建.
为了对领先的动画库,当然还要包含Transit(它使用CSS渐变效果),进行直接的对比,回头去看看Velocity在VelocityJS.org上的文档.
问题仍然是:JavaScript是怎样具体的达成其高水平性能的?下面是对基于Javascript动画能够被执行这一目标的优化的一个简短清单:
- 同步DOM→在整个动画链中间入栈以最小化布局抖动.
- 为整个链式调用缓存属性值,以最小化DOM查询发生(这些就是高性能DOM动画的坑).
- 在同样的调用中缓存整个同级别元素的单元转换率(比如px到%,em,等等.).
- 当更新可能会在视觉上不可见时跳过样式更新.
回顾一下我们先前学过的关于布局颠簸的知识,Velocity.js利用这些最佳实践来缓存动画结束值以复用为随后动画的开始值,从而避免了重新查询DOM以获取元素的开始值:
$element /*Slidetheelementdownintoview.*/ .velocity({opacity:1,top:"50%"}) /*Afteradelayof1000ms,slidetheelementoutofview.*/ .velocity({opacity:0,top:"-50%"},{delay:1000});
在上面例子中,第二个Velocity调用知道它应该自动从opacity为1和top为50%开始。
浏览器本身最终能够执行许多这些相同的优化,但这样做会明显减少开发者能够制作的动画代码的方式。因此,出于同样原因,由于jQuery不使用RAF(如上所述),浏览器就不会强制优化它,甚至给出一个很小的机会去打破规格或偏离预期的行为。
最后,我们对这两个JavaScript动画库(Velocity.js和GSAP)互相比较一下。
GSAP是首个动画库,用在演示JavaScriptDOM令人印象深刻的动画表现。它确实是这样,但有些缺点:
- 在中到高负荷动画中,GSAP的DOM交互开销导致动画在开始时和过程中失帧。
- 相反于Velocity.js是在超宽松的MIT许可下发布的, GSAP是闭源的,并且在很多类商用时候需要许可年费。
- 因为GSAP是一个完整的动画套件,是Velocity大小的三倍。然而,GSAP有如此丰富功能,有助于其成为动画的瑞士军刀。
我推荐做法是在你需要精确控制定时(比如重绘,暂停/恢复)和运动(比如贝塞尔曲线路径)的时候用GSAP。这些特性在游戏开发和某些特殊应用中是至关重要的,但是通常不需要用在网页应用的UI中。
Velocity.js
引用GSAP丰富的特性并不代表Velocity自身在特性上是轻量级的.相反,在压缩后仅有的7kb中,Velocity不仅仅复制了jQuery$.animate()的所有功能,它还把颜色动画,转换,循环,easing效果,类动画还有滚动都打包了进去.
总之,Velocity是jQuery,jQueryUI,以及CSS渐变效果的最佳组合.
此外,从便利的角度看,Velocity在hood(盖子,大概意思是公共的接口)之下使用jQuery的$.queue()方法,如此就可以实现同jQuery的$.animate(),$.fade(),和$.delay()函数的无缝互操作.而且,由于Velocity的语法同$.animate()的语法是相同的,你不需要改变页面的任何代码.
让我们快速地来看一看Velocity.js.在基础的层面,Velocity的行为同$.animate()一样:
$element .delay(1000) /*UseVelocitytoanimatetheelement'stoppropertyoveradurationof2000ms.*/ .velocity({top:"50%"},2000) /*UseastandardjQuerymethodtofadetheelementoutonceVelocityisdoneanimatingtop.*/ .fadeOut(1000);
在其最高级的层面,可以创建带有3D动画的复杂滚动场景—几乎只要用到两行简单的代码:
$element /*Scrollthebrowsertothetopofthiselementoveradurationof1000ms.*/ .velocity("scroll",1000) /*ThenrotatetheelementarounditsYaxisby360degrees.*/ .velocity({rotateY:"360deg"},1000);