本文是求索:GSAP的动画快于jQuery吗?为何? 的续文。GSAP是一个js动画插件,它声称“20x faster than jQuery”,是什么让它这么快呢?
每当有这样的问题的时候,我们可以通过以下步骤来确定一个未知的解决方案的性能优化是怎么
做到
/伪造
的:
- 黑盒:从官方用例来看,究竟有多快,快在哪儿
- 白盒:看看官方用例之内,框架怎么做到优化的
- do { 提出假设,自己构建用例测试 } while (假设没有得到验证);
- 得出结论
本文是整个探究过程的后两个部分。
对前文的一些补充
本文可能需要你知道的一些知识
-
requestAnimationFrame相关:
-
浏览器工作原理
-
浏览器提供的调试工具
注:这里没有更好的inspector快照内容介绍(翻译的,或是足够详尽的),如果你知道的话不妨告诉我哟~
为了下文阅读方便,这里从profiler得到的结果角度,提一下浏览器里,从定时器被激活,到用户看到图形的主要流程:
CHROME
脚本运行(黄色)->回流与重计算(紫色)->重绘(绿色)->其他(白色,探查器的相关内容)
IE11
加载(深蓝色)->脚本运行(红色)->内存垃圾回收(黄色)->回流与重计算(绿色)->重绘(紫色)->其他(灰色,探查器的相关内容)
注意到chrome和IE11的重绘与回流/重计算的表示颜色是相反的。
下文中的测试,chrome的计时区间是一个gc的间隔(50s左右)(因为在chrome里,FPS和内存占用有比较小的反相关,内存占用越多,FPS越低),而IE则采用10s的间隔(因为在IE 11里,FPS和内存占用看不出相关性)。
同时,不对不支持requestAnimationFrame的浏览器做出评测。
注:这里也没有测试firefox,因为目前还没有找到靠谱的测量UI响应的功能,如果有请@我,多谢
最后,为了下文比较方便,我们在chrome和IE11下再次测试官方给的用例,获得表格(单位FPS,为平均值,后面均一样,不再次说明):
浏览器 | chrome | IE 11 |
GSAP test: jQuery | 14 | 22 |
GSAP test: GSAP | 45 | 53 |
假设与验证,自己构建测试用例,看看优化是否名不副实
根据前文所述,我们得到以下假设:
- jQuery的定时器采用的是
setInterval
,受到浏览器重绘上限的控制,而GSAP采用requestAnimationFrame
,完全将重绘交给浏览器管理,以获得更好地重绘性能 - jQuery每次都是单独修改一个DOM的style,而GSAP是计算离线的style,然后再赋给DOM。这导致了不一样的时间开销。
- jQuery没有集中绘制,每个DOM都在一个事件回调函数上下文中处理,有多少个DOM就有多少个上下文;GSAP有集中绘制。同时jQuery是过程化的,GSAP是面向对象的。这让jQuery非常难以做到集中控制绘制。jQuery会将DOM的引用一路传递到最终改变DOM的style函数中,这在调用过程中也会非常浪费空间。这些也都导致了不一样的时间开销。
是这样的吗?
测试1:setTimeout vs requestAnimationFrame
之前已经有这样的测试了:《setInterval与requestAnimationFrame的时间间隔测试》,但有以下弊病:
- 在那个测试没有进行实际的任务,仅仅是空循环判断时间而已
——改进:在这里我做了一个绘制随机div的操作,一方面尽量接近GSAP的用例,另一方面尽量简短,以减少变量对测试的影响。 - 测试所用的时间测量非常不准确
——改进:测量结果,用的是chrome和IE11浏览器本身提供的原生性能分析工具,以取得尽量准确的帧率。
<style>
body{ position:relative; height:100% ; background:#000; overflow:hidden; }
div{ width : 4px; height : 4px; border-radius : 100%; position : absolute; }
</style>
在body内部填入N(N=500,可以改变这个值)个div,设置它们的圆角、背景属性。然后在每帧里面随机改变他们的top
、left
(对应着回流)、transform
(对应着重绘)值,这样,可以让每帧的绘制里页面常见的绘制操作耗时均等。大致就是要绘制以下的效果。
测试代码如下:
http://jsfiddle.net/humphry/HNysW/4/
CHROME结果:
setTimeout(fn,10)
setTimeout(fn,13)
setTimeout(fn,16)
requestAnimationFrame
setInterval(fn,16)
- “我去,帧率没有太大区别嘛。”
- “该不是因为我们没有测IE嘛?”
恩,那么我们进入IE11,。下图是requestAnimationFrame的测试结果,可惜没有统计,也不支持导出数据,不能知道平均帧率。
我将setInterval的结果印在requestAnimationFrame上,两者的对比可见下图:
- “并没有没有显著的提升啊。”
- “看起来一定是我们没有够数据量的缘故。”
来吧,调节点的数量:
我们现在放进去1000个小点,我们在IE11下的结果对比以下:
- “要说有那么一点优化,却是一点优化都看不出来的感觉呢。”
- “看起来一定是我们没有测小绘制压力的缘故。”
或者调节成200个小点呢,我们在IE11下帧率对比一下:
再综合一下数据:
小点个数 | 200 | 500 | 1000 | |||
浏览器 | chrome | IE 11 | chrome | IE 11 | chrome | IE 11 |
requestAnimationFrame | 53 | 60 | 30 | 32 | 17 | 19 |
setInterval 16 | 53 | 60 | 29 | 30 | 12 | 19 |
- “为什么,为什么看不出来效果呀 擦 擦”
- “阿玛,我们不是被涮了吧!”
- “不可能!他们说得真真儿的,究竟是什么地方不对呢……我们是不是把回流和重绘弄得太均匀了啊”
测试2:setTimeout vs requestAnimationFrame对回流的影响的测试
……好吧,那么我构造一个这样的文档结构,div
套div
套div
套div
套……让它们都为inline-block
,以获得包裹的效果……
body{ height:100% ; background:#000; overflow:hidden; }
html{ height: 100%; }
div{ padding: 1px 3px 2px 0; border-top: 2px solid; display: inline-block; }
大概像是这样。
这个时候改变最里面的div的宽度,就会导致大面积的回流了。
http://jsfiddle.net/humphry/SwLY7/1/
可以看到,占大比例的是回流(紫色部分)。hover到layout处,可以看到:
全员参与回流,CPU什么的一定很带感呢。
结果:
参与嵌套的DIV数量 (reflow) | 200 | 500 | 700 | 1000 | ||||
浏览器 | chrome | IE 11 | chrome | IE 11 | chrome | IE 11 | chrome | IE 11 |
requestAnimationFrame | 48 | 60 | 26 | 60 | 15 | 60 | 8 | (崩溃) |
setInterval 16 | 47 | 60 | 26 | 60 | 15 | 60 | 8 | (崩溃) |
测试3:setTimeout vs requestAnimationFrame对重绘的影响的测试
我们来设置阴影,以期获得较长的重绘时间:
一闪一闪亮晶晶~
http://jsfiddle.net/humphry/XdFqR/1/
如图,绿色的重绘占据了大部分。
结果:
闪烁的星星数量(repaint) | 80 | 100 | 200 | |||
浏览器 | chrome | IE 11 | chrome | IE 11 | chrome | IE 11 |
requestAnimationFrame | 54 | 16 | 45 | 12 | 26 | 4 |
setInterval 16 | 47 | 16 | 38 | 10 | 24 | 5 |
- “……”
- “……尽管在chrome下是有一些提升,但是也没有达到用例那么明显,达到两倍的关系呀阿玛。”
我们来再回顾一次结果:
我们前面的三个测试:
- 随机重排、缩放圆点测试:测试回流+重绘
- 嵌套inline-block大面积回流测试:测试回流
- 随机阴影模糊半径测试:测试重绘
基本上可以说明,在低渲染压力/中等渲染压力/高渲染压力三种场景中,requestAnimationFrame
里面的重绘性能平均比setInterval
快1~5帧左右,而回流性能则没有很大影响。
那么,GSAP是如何让用例出现这么大的区别呢?chrome里快2倍,IE11里面快1倍,这不是一个轻易换用requestAnimationFrame就可以达到的结果。
我想到,setInterval这个函数其实并不是一次触发,而是针对每个DOM触发的。在此之前的所有测试,皆是集中的计时器。因此,有了第四个测试:
测试4:集中计时器 VS 分散计时器 VS 集中rAF VS 分散rAF
第四个测试,就是将前面的第一个和第三个测试改一下,将集中改成分散。方案是,扩展HTMLDivElement原型方法,然后让每个DOM调用它,即可模拟在官方的jQuery测试用例中发生的事情。
HTMLDivElement.prototype.startAnimationOnMyOwn = function() {
var that = this ;
setInterval(function(){
repaint(that) ;
} , 16) ;
} ;
for (var i = 0; i < NUM; i++) {
allnodes[i].startAnimationOnMyOwn() ;
}
测试1的去中心化版:
http://jsfiddle.net/humphry/pBwBx/
测试3的去中心化版:
http://jsfiddle.net/humphry/7fa64/
得到结果:
测试1·改(去中心化的重绘/requestAnimationFrame)
圆点数量 | 200 | 500 | 1000 | |||
浏览器 | chrome | IE 11 | chrome | IE 11 | chrome | IE 11 |
rAF (集中) | 53 | 60 | 30 | 32 | 17 | 19 |
rAF (去中心化) | 17 | 20 | 12 | 2 | 6 | 1 |
setInterval 16 (集中) | 53 | 60 | 29 | 30 | 12 | 19 |
setInterval 16 (去中心化) | 19 | 34 | 8 | 15 | 4 | 6 |
测试3·改(去中心化的重绘/requestAnimationFrame)
闪烁的阴影数量 | 80 | 100 | 200 | |||
浏览器 | chrome | IE 11 | chrome | IE 11 | chrome | IE 11 |
rAF (集中) | 54 | 16 | 45 | 12 | 26 | 4 |
rAF (去中心化) | 33 | 13 | 27 | 12 | 16 | 5 |
setInterval 16 (集中) | 47 | 16 | 38 | 10 | 24 | 5 |
setInterval 16 (去中心化) | 36 | 14 | 31 | 10 | 14 | 5 |
这个表中终于出现了非常大的数据波动,也符合GSAP的的测试结果,我们可以得出结论了。
得出结论
比较直观的所有结果比较:
-
setInterval是否比requestAnimationFrame更慢?
不是的。上表中的数据可以表明这一点,在短的回调里,造成重绘的相关代码,requestAnimationFrame比setInterval稍微快一些;但是在长回调函数(多次构造requestAnimationFrame,它们会被合到一个里面)里,requestAnimationFrame不比setInterval快。
这也是《理解WebKit:渲染主循环main loop和rAF》里说:“回调函数不能太大,不能占用太长时间,否则会影响页面的响应和绘制的频率”的原因。
requestAnimationFrame最主要的意义,是降帧而非升帧,以防止丢帧。它的目的更类似于垂直同步,而非越快越好。MSDN: 帧率不等或跳帧会使人感觉你的站点速度缓慢。如果降低动画速度可以减少跳帧并有助于保持帧率一致,它可以使人感觉站点速度更快。
阅读更多:http://creativejs.com/resources/requestanimationframe/ 集中定时器造成重绘是否比分散定时器造成重绘快?
是的。这也是GSAP更快的原因。测试需要改进吗?
我认为需要。测试过程中为了比较好的效果用了随机数。其实生成随机数的过程中也耗费了一定的时间,更好的测试中,可以用线性的变化替换随机离散的数值变化,数据会更加稳定。
同时,没有测试firefox,很可惜,到目前为止,笔者依然没有找到一款好使的分析UI的插件。GSAP的用例是否说明了GSAP快于jQuery呢?
是的,这可以说明GSAP更快。但并非是在任何时候都更快。在我们需要粒子系统时快,在我们只需要绘制一两个小交互时,它没有提供非常明显的性能优化的可能性。后者可以直接用CSS3 Animation/Translation做,或者用jQuery达到全浏览器兼容。若出现了粒子系统这样的大量元素重绘的需求,用GSAP是很好的选择。
造成GSAP更快的原因,是由于jQuery的处理方式,非常不适合绘制大量节点。
再次摘抄用例:
tests.jquery = {
tween:function(dot) {
dot[0].style.cssText = startingCSS;
var angle = Math.random() * Math.PI * 2;
dot.delay(Math.random() * duration).animate({left:Math.cos(angle) * radius + centerX,
top:Math.sin(angle) * radius + centerY,
width:32,
height:32}, duration, "cubicIn", function() { tests.jquery.tween(dot) });
}
};
function toggleTest() {
i = dots.length;
while (--i > -1) {
currentTest.tween(dots[i]);
}
}
在正常的项目执行过程中,我们会使用jQuery.animate绘制大量元素吗?有人可能会,但我不会这么做。
GSAP相对于jQuery的进步性,就在于集中绘制了所有需要动画更新的元素;同时也有更多的插件供选择,可以每过一帧改变更多的类型,而非仅仅是CSS样式。
在项目中引入GSAP,需要引入以下三者:
<script src="http://cdnjs.cloudflare.com/ajax/libs/gsap/1.11.4/plugins/CSSPlugin.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/gsap/1.11.4/easing/EasePack.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/gsap/1.11.4/TweenLite.min.js"></script>
耗费三个连接,和近25Kb来加载这个组件,获得在有很大绘制任务时更快的动画实现,这个代价值得不值得,还是需要在具体需求具体分析了。
在最后,推荐一篇高大上全的文章:《编写快速、高效的JavaScript代码》。引用文中的一句话,做结语吧:
“正如我们所见,在JavaScript的引擎世界里面,有许多的隐藏的性能陷阱。但事实上并没有性能提高的银弹。只有当你在测试环境中结合一系列的优化,你才会意识到最大的性能获益。”
更新list
v1.1
前端很多测试都不乏前人,早在HTML4时代就有人测试GUI Benchmark了:GuiMark,从他给出的用例来看,他主要测试了回流和重绘同时存在的情形。也可以参考一下测试方式。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。