JavaScript动画的性能并不亚于CSS动画。所以,若是使用了现代的动画库,例如Velocity,那么动画引擎的性能将再也不是app的瓶颈,构成瓶颈的只有代码。javascript
网络性能相关css
动画是浏览器运行中资源很是密集的进程,可是有不少技术可以帮助浏览器尽量高效地运行。下面会提到这些技术。html
性能影响一切。java
可是对于用户来讲,他们的设备配置良莠不齐,不可能都像开发人员同样用最新版iPhone。因此,要考虑的就是在低端设备上提供较为流畅的体验。还有,有时只考虑设备处于理想负载下的状况,但实际上,用户的浏览器可能开了不少应用和选项卡,这在必定程度上也须要流畅体验。编程
一.去除布局颠簸数组
布局颠簸,就是DOM操做缺少同步性,是拖垮动画性能的很主要的因素。对它虽没有轻松的解决办法,但却有最佳实践。能够继续来看。浏览器
看一下网页操做是如何进行设置(setting)和获取(getting)这两项任务的:能够设置(更新)或获取一个元素的CSS属性。同理,能够往页面里插入新元素或者从页面里查询一组已存在元素。获取和设置是引起性能开销的两个核心浏览器进程(另外还有图形渲染)。能够这样来想这个问题:在为元素设置了新属性之后,浏览器必须计算此次更改所产生的后续影响。例如,改变一个元素的宽度会致使一系列连锁反应;它的父级元素、兄弟元素和子元素的宽度根据各自的CSS属性也要调整。
由设置和获取的交替而致使的UI性能下降被称为布局颠簸。尽管浏览器已经为页面布局的从新计算进行了高度优化,但因为布局颠簸,这些优化的效果大打折扣。例如,浏览器能够轻易地将同一时间的一系列获取操做优化成一个单一的、流畅的操做,这是由于浏览器在第一次获取以后能够缓存页面的状态,而后在后续每次获取操做时,参考那个状态。可是,若是反复执行了获取以后又执行设置,就会让浏览器去作许多繁重的工做,由于设置所作的更改会不断地使其缓存失效。缓存
当布局颠簸在动画循环中出现的时候,对性能的影响更为厉害。假设一个动画循环力求达到60帧每秒,这是人眼感知平滑运动的最低值。这意味着在动画循环中,每个tick都必须在16.7毫秒(1秒/60tick≈16.67毫秒)内完成。布局颠簸很容易致使每一个tick超过这个时限。最终结果固然就是动画变得卡顿。尽管有些动画引擎,例如Velocity.js,在其动画循环中为减小布局颠簸进行了优化,但还要小心在你本身的循环中避免出现布局颠簸,例如在setInterval()或自调用的setTimeout()代码里面。性能优化
解决:网络
方法就是把DOM的设置和获取的操做分别集合在一块儿。如下代码会致使布局颠簸:
// 糟糕的作法 var currentTop = $("element").css("top"); // 获取 $("element").style.top = currentTop + 1; // 设置 var currentLeft = $("element").css("left"); // 获取 $("element").style.left = currentLeft + 1; // 设置
若是重写上述代码,把查询放在一块儿,把设置放在一块儿,那么浏览器就能够打包相应的操做,从而减小代码形成的布局颠簸的影响:
var currentTop = $("element").css("top"); // 获取 var currentLeft = $("element").css("left"); // 获取 $("element").css("top", currentTop + 1); // 设置 $("element").css("left", currentLeft + 1); // 设置
或者:
var currentTop = $("element").css("top"); // 获取 var currentLeft = $("element").css("left"); // 获取 $("element").css({ "top": currentTop + 1, "left": currentLeft + 1 }); // 设置
以上所说明的问题常常会在生产代码中看到,尤为是当UI操做依赖于元素当前CSS属性值的时候。
好比你的目的是在单击按钮的时候,切换侧边菜单的可见性。要想达到这一效果,你可能会先检查侧边菜单的display属性是设置成"none"仍是"block",而后再相应地进行值的替换。检查display属性的过程构成一次“获取”;后续不管是将侧边菜单显示出来仍是隐藏起来都构成了一次“设置”。
要想优化这种代码就必须在内存中保留一个变量,每当按钮点击时,这个变量跟着更新,而后在切换可见性以前,经过查询这个变量得知侧边菜单的当前状态。这样,“获取”的过程就彻底省掉了,从而有助于减小设置和获取交替出现的可能性。另外,除了下降布局颠簸发生的可能性之外,UI如今还得益于减小了一次页面查询。记住:每次设置和获取对于浏览器操做来讲都比较消耗性能;设置和获取次数越少,UI的速度就会越快。许许多多的小改进最终会积累成至关可观的好处,而这正是本文的潜在主题:尽量多地遵循性能最佳实践,就能够尽量少地为了性能而妥协本身心中理想的动效设计目标,从而实现满意的页面。
Jquery元素对象
若是网页使用了Jquery,实例化Jquery对象也是形成DOM获取操做的一个因素。
好比:
$("#element").css("opacity", 1);
或者等效的原生JavaScript:
document.getElementById("element").style.opacity = 1;
在jQuery代码中,由$("#element")返回的值就是一个JEO,即一个包装了所查询的原生DOM元素的对象。JEO提供了全部你欢喜的jQuery功能,包括.css()、.animate()等。
原生代码中,getElementById()返回的是一个没有包装过的DOM元素,上面两种写法都要求浏览器搜索DOM树,找到想要的元素。这种操做,若是重复屡次,就会影响页面的性能。
当未被缓存的元素在重复使用的代码片断中出现,例如在循环代码中,对性能的影响就更严重了。下面这个例子:
$elements.each(function(i, element) { $("body").append(element); });
each中反复访问$(body),会影响性能。再者每次循环都会append()一个元素,致使一次重排版,也会影响性能。
解决这两个问题的方法,分别是缓存Jquery包装对象和批量操做DOM:
// 糟糕作法:未缓存JEO $("#element").css("opacity", 1); // …… 一些中间代码…… // 咱们再次将JEO实例化 $("#element").css("opacity", 0);
缓存Jquery包装对象:
// 缓存jQuery元素对象,在变量前面加个前缀$用来表示这是个JEO var $element = $("#element"); $element.css("opacity", 1); // …… 一些中间代码 …… // 咱们复用了缓存的JEO,避免了一次DOM查询 $element.css("opacity”, 0);
在后面的代码里面能够一样使用$element.
强制给值:动画引擎的传统作法是在动画的一开始查询一遍DOM来肯定每一个被设置动画的CSS属性的初始值是多少。Velocity经过一种称为“强制给值”的功能能够绕过这一页面查询事件。这也是避免布局颠簸的另外一项技术。经过强制给值,能够明确地为动画设置初始值,从而完全免去了一开始就对页面进行获取的操做。强制给定的值做为第二项被传入一个数组中,而这个数组替代了本来动画属性映射中属性值的位置。数组中的第一项是你想要设置动画变更到的最终值。
批量添加DOM
有一种常见的页面设置操做是在页面运行时插入新DOM元素。为页面添加新元素有不少用途,不过其中最流行的也许就是无限滚动了,它在用户向下滚动的时候,不断让新元素在页面底部以动画方式进入视图。在前面已经知道,每当有一个新元素添加进来,浏览器就必须针对全部受到影响的元素进行计算。这是一个相对较慢的过程。所以,当每秒要进行屡次DOM插入时,页面的性能就会受到显著影响。幸运的是,当处理多个元素时,若是全部元素是同时插入的,那么浏览器能够对这个设置的操做进行优化。但不幸的是,做为开发人员的咱们常常无心识地放弃了这种优化作法,给DOM单独添加元素。请看下面未优化的DOM插入作法:
// 糟糕的作法 var $body = $("body"); var $newElements = [ "<div>Div 1</div>", "<div>Div 2</div>", "<div>Div 3</div>" ]; $newElements.each(function(i, element) { $(element).appendTo($body); // 其余代码 });
以上代码遍历了一组元素字符串,这组元素字符串被实例化到jQuery元素对象中。(这么作没有什么性能损失,由于你没有针对每一个JEO去查询DOM。)而后,使用jQuery的appendTo()函数将每一个元素插入到页面中。
问题是这样的:即便在appendTo()语句后面还有其余代码,浏览器也不会把这些DOM设置操做压缩成一个单一的插入操做,由于浏览器不能肯定循环之外的异步代码操做不会在插入操做之间修改DOM状态。例如,想象这样一个场景:在每次插入以后都查询DOM,想要搞清楚究竟有多少元素在页面上存在:
// 糟糕的作法 $newElements.each(function(i, element) { $(element).appendTo($body); // 输出body元素有多少个子元素 console.log($body.children().size()); });
浏览器没法将上面的DOM插入优化成一次操做,这是由于代码明确要求浏览器告诉咱们,在下次循环开始以前,究竟存在多少元素。由于浏览器每次都要返回正确数值,所以它没法批量处理后面全部的插入操做。
总之,在循环内部进行DOM元素插入时,每一次插入的操做与其余都是互相独立的,所以会形成明显的性能损失
解决方法就是,不要将一个元素直接插入DOM中,先构建一个完整的DOM集合,而后一次性插入到页面中去。前面举的例子能够优化成:
// 优化后 var $body = $("body"); var $newElements = [ "<div>Div 1</div>", "<div>Div 2</div>", "<div>Div 3</div>" ]; var html = ""; $newElements.each(function(i, element) { html += element; }); $(html).appendTo($body);
上面代码还有能够优化的地方,字符串拼接能够继续优化。
以上代码将表明每一个HTML元素的字符串连在一块儿造成一个主字符串,而后把这个主字符串转成JEO并一次性添加到DOM上。经过这种作法,浏览器获得明确指示,将全部元素一次性插入,相应的性能也获得了优化。
避免影响临近的元素:
提高性能很重要的一点就是要考虑一个元素的动画对其临近元素的影响。
例如,若是夹在两个兄弟元素之间的一个元素宽度缩小,那么它的兄弟元素的绝对定位就会动态改变,从而保持在动画元素的旁边。另外一个例子多是设置嵌入在父元素中的子元素的动画,而这个父元素并无明肯定义的width和height属性。相应地,设置子元素的动画时,父元素的尺寸也会改变,从而确保将子元素彻底包裹在内。实际上,子元素并非惟一被设置动画的元素,由于它的父元素的尺寸也被设置了动画。若是这发生在动画循环里面,那么浏览器在每次循环时要作的工做就更多了!
有不少CSS属性,一经改变,就会形成临近元素尺寸或位置的调整,其中包括:top、right、bottom和left,全部的margin和padding属性,border厚度,以及width和height尺寸。做为关心性能的开发人员,须要了解设置这些属性的动画会给页面带来什么影响。时刻问本身,设置每一个属性的动画会怎样影响临近元素。若是重写代码可以让你避免元素变化带来的互相影响,那么请考虑重写。事实上,要这么作有一种简便方法,继续看后面的解决办法!
解决方法:
这种能够避免影响到临近元素的解决办法是尽量设置CSS的transform属性(translateX、translateY、scaleX、scaleY、rotateZ、rotateX和rotateY)的动画。transform属性的特殊之处在于它们将目标元素提高至一个单独的层,这个层能够独立于页面其余内容单独渲染(经过GPU加速提高性能),所以相邻的元素不会受到影响。例如,在设置一个元素的translateX变更到"500px"的动画时,元素会向右移动500像素,覆盖在任何动画路径上已经存在的元素的上面。若是在动画路径上没有任何元素(也就是没有相邻的元素),那么使用translateX的效果与设置更慢的left属性的动画的效果,在页面上看起来是同样的。
因此浏览器支持的状况下,本来这样:
// 将元素自左侧移动500像素 $element.velocity({ left: "500px" });
就能够写成这样:
// 更快:使用translateX $element.velocity({ translateX: "500px" });
top也是相似的:
$element.velocity({ top: "100px" }); // 更快:使用translateY $element.velocity({ translateY: "100px" });
减小并发加载:
当页面首次加载时,浏览器会尽量快地处理HTML、CSS、JavaScript和图片。所以不出意外,这时候发生的动画容易发生延迟,它们在努力抢夺浏览器有限的资源。因此,尽管在页面加载序列中添加动画是显摆动效设计技巧的好时机,但若是想避免用户产生网站很慢的第一印象,那么克制本身不要这么作。同理,当许多动画同时在页面上发生时,也会出现一个相似的并发性瓶颈,不论它是出如今页面生命周期中的哪一个阶段。在这些状况下,浏览器在同时处理众多样式变化的重压下会喘不过气来,而后卡顿就发生了。
错开动画:
减小并发动画加载的一个方式是使用Velocity的UI pack中的stagger功能,它会相继在一组元素的动画开始前添加指定的延迟时间。例如,要设置一组元素中每一个元素的opacity值变更至1的动画,而且在动画开始时间之间相继添加300毫秒的延迟,代码可能会是这样:
$elements.velocity({ opacity: 1 }, { stagger: 300 });
这时候,这些元素再也不是彻底同步执行动画的,而是在整个动画序列的开头,只有第一个元素在执行动画。而后,在整个序列的结尾,只有最后一个元素执行动画。你很高效地分散了动画序列的总工做量,使浏览器老是在每一刻作更少的工做,而不是同时执行每一个元素的动画,让浏览器累得喘不过气来。另外,在动效设计中使用错开动画,一般会获得较好的审美效果。
多动画序列:
减小并发加载还有另外一个方法:将多个属性的动画拆成多动画序列。以设置元素的opacity值的动画为例。这一般是一个相对轻松的操做。可是,若是同时还要设置元素的width和box-shadow属性的动画,那么就会给浏览器带来更多可观的工做量:会影响更多像素,也要进行更多计算。所以,若是本来动画像这样子:
$images.velocity({ opacity: 1, boxShadowBlur: "50px" });
能够改写成:
$images .velocity({ opacity: 1 }) .velocity({ boxShadowBlur: "50px" });
这样浏览器就有更少的并发工做要作,由于这些都是一个接一个发生的单独属性动画。注意此处要进行权衡,由于整个动画序列的持续时间变长了。这对于最终的应用场景而言,也许是好事,也许是坏事。
既然这种优化须要改变你本来对动效设计的想法,那么这一技巧并不是老是要使用。把它做为最后的手段吧。若是须要在低端设备上挤出额外的性能,那么用这种技巧或许合适。其余状况下,不要用这种技巧预先优化网站上的代码,不然的话,最终获得的将是没必要要的臃肿且晦涩的代码。
不用持续响应滚动(scroll)和调整大小(resize)事件
对于高频事件,最好使用防抖动控制发生频率。浏览器的滚动(scroll)和调整大小(resize)是两个触发频率很是频繁的事件类型:每当用户调整或滚动浏览器窗口时,浏览器都会在每秒内触发屡次与这些事件相关的回调函数。所以,若是你注册的回调函数与DOM有交互的话,或者更糟,包含布局颠簸的话,那么它们会在滚动或调整大小时带来巨大的浏览器负担。请看下面的代码:
// 当滚动浏览器窗口时,执行一个行为 $(window).scroll(function() { // 这里写的任何行为都会在用户滚动时,每秒钟触发屡次 }); // 当浏览器窗口的大小改变时,执行一个行为 $(window).resize(function() { // 这里写的任何行为都会在用户调整窗口大小时,每秒钟触发屡次 });
解决方式就是加防抖动。防抖动就是,定义一个时间间隔,在此时间间隔期间,事件句柄回调将仅会被调用一次。例如,假设你定义了一个250毫秒的反跳间隔,而用户滚动页面的总持续时间为1000毫秒。这时候,进行了防抖动的事件句柄代码就会相应地仅触发四次(1000毫秒/250毫秒)。
若是不想本身写原生js来防抖动,不少库能够用,好比的Underscore.js(UnderscoreJS.org),它是一个与jQuery很相近、也提供用于简化编程的辅助函数的JavaScript库,这个库含有debounce函数,你能够轻松地在事件句柄上反复使用它。
一些代码是这样:
// MooTools Function.implement({ debounce: function(wait, immediate) { var timeout, func = this; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; var callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } }); // Use it! window.addEvent("resize", myFn.debounce(500))
不过如今部分浏览器,如Chrome的最新版本已经自动反跳滚动事件了。
减小图片渲染:
视频和图片是多媒体元素类型,浏览器必需要加倍努力渲染才行。要计算非多媒体元素的尺寸属性是很轻松的,可是多媒体元素包含成千上万的像素数据,要改变它们的大小、尺寸或是从新合成,对浏览器而言计算开销是很大的。设置这些元素的动画的性能老是比不上设置标准HTML元素(如div、p和table)的动画的性能,来得理想。另外,鉴于滚动页面几乎能够视为设置整个页面的动画(能够把滚动页面视为设置页面的top属性的动画),在CPU不高的移动设备上,多媒体元素也会形成滚动性能的巨幅降低。
另外,鉴于滚动页面几乎能够视为设置整个页面的动画(能够把滚动页面视为设置页面的top属性的动画),在CPU不高的移动设备上,多媒体元素也会形成滚动性能的巨幅降低。
解决:
不幸的是,除了尽量把简单的、基于图形的图片转成SVG元素之外,就没有其余任何办法能够将多媒体内容重构成更快的元素类型。所以,惟一可行的性能优化作法就是减小在页面上同时显示和同时设置动画的多媒体元素总数。注意这里用到的同时一词是在强调浏览器渲染的客观状况:浏览器只渲染能够看到的东西。页面上看不到的部分(包括包含额外图片的部分)是不会被渲染的,并且也不会对浏览器进程形成额外压力。所以,有两种最佳实践能够遵循:第一种,若是本来感受在页面上添不添额外图片都无所谓的话,那么选择不添。要渲染的图片越少,UI性能就越好。(更不用说更少的图片给页面网络加载时间带来的正面影响。)
第二种,若是你的UI在同时加载不少图片到视图(好比,8幅或以上,根据设备硬件性能而定),考虑不要设置这些图片的动画,或者只是简单地切换每幅图片的可见性从不可见到可见。这种视觉效果可能并不优雅,要弥补这一点,能够考虑错开切换可见性的动画时间,使图片一个接一个显示而不是同时显示出来,这样作一般会产生出更精致的动效设计。
除了img元素,还有其余的形式,图片显示到页面上的形式。
CSS渐变:渐变其实是图片的一种。它们不是用图片编辑器事先生成的,而是根据CSS的样式定义,在运行时生成的,例如在一个元素的background-image属性上用了linear-gradient()做为值。这里的解决办法是尽可能选择纯色而非渐变背景。浏览器能够轻松优化纯色色块的渲染,可是就像对待图片同样,浏览器渲染渐变也格外费力,由于渐变的色彩是逐像素变化的。
阴影属性:渐变有个坏坏的双胞胎,那就是box-shadow和text-shadow这两个CSS属性。它们的渲染跟渐变的渲染大同小异,只不过不是在background-color上,而是在border-color上罢了。更糟糕的是,它们的不透明度还逐渐减小,这要求浏览器进行额外的合成工做,由于渐变的半透明部分必须依据动画元素下面的元素来渲染。这里的解决办法跟以前的差很少:若是从样式表上移除这些CSS属性后,UI的视觉效果跟以前差很少优秀,那么宽慰一下本身,放弃以前的方案吧。网站的高性能会反过来回报你的。
这些建议只是建议而已。它们并不是性能最佳实践,由于你要为了提升性能而牺牲设计本意。只有当网站性能很糟糕的时候,才考虑使用这些没有办法的办法。
在旧浏览器上降级动画:
IE系列浏览器在本国还在普遍被使用,低版本的IE也会占到必定份额。另外,运行着Android 2.3.x及更早系统的老安卓智能手机比最新一代的Android和iOS设备要慢,但它们依然被普遍使用。相应地,若是你的网站有丰富的动画和其余UI互动,那么就能够推断对于这块用户来讲,网站的运行很糟糕。
解决:
要解决低端设备形成的性能问题有两种方式:要么无论三七二十一减小整个网站的动画;要么只针对低端设备减小动画。前者说究竟是一种产品决策,然后者则只是一种能够轻松实施的技术决策,只要使用了全局动画乘数技术(或Velocity中对应的mock功能)。全
局乘数技术使你可以经过一个变量改变整个网站的动画时间。所以,这里的诀窍就是:每当检测出用户正在使用性能较弱的浏览器,那么就将乘数设置为0(或者将$.Velocity.mock设置为true)。这样作就能让整个页面的动画都在一个动画tick(少于16毫秒)中完成:
// 使全部动画当即完成 $.Velocity.mock = true;
这样,在性能较差的浏览器中,本来的动画渐变变成样式的当即修改,会流畅一些。
另外,找到性能门限,在参考设备上测试性能,也相当重要,
参考:《javascript网页动画设计》