本文已完结,请看下文: > 求索:GSAP的动画快于jQuery吗?为什么?/续 css
本文源自对问题《GSAP js动画性能优于jQuery的原理是什么?》的回答。GSAP是一个js动画插件,它声称“20x faster than jQuery”,是什么让它这么快呢?html
每当有这样的问题的时候,咱们能够经过如下步骤来肯定一个未知的解决方案的性能优化是怎么作到
/伪造
的:jquery
文中提到的timer、recalculate、layout、repaint、composite layer,须要浏览器内部运行相关的基础知识。见:web
首先咱们打开chrome,并开启官网的H5动画速度测试页面:http://www.greensock.com/js/speed.html。chrome
页面中用js计算出的fps很不许确,仍是以浏览器的统计为准。segmentfault
在jQuery和GSAP两个框架下打开,而后点run,而后f12审查元素,进入Timeline页面,点record。过了100frame之后暂停,而后进入页面点击stop。浏览器
如下是jQuery的结果:100帧6.53s,平均FPS:15帧/秒
(也能够本身算出来 100frames ÷ 6.53s ≈ 15.3FPS
)缓存
如下是GSAP的结果:100帧2.22s,平均FPS:45帧/秒。比jQuery快2倍呢。性能优化
来对比一下100帧里面各个流程的耗时(单位:秒):网络
类目 | 详情 | jQuery | GSAP |
scripting | timer等js执行 | 2.87 | 0.52 |
rendering | recalculate(重计算)、layout(回流) | 2.04 | 0.77 |
painting | repaint(重绘)、composite layers(混合图层) | 0.88 | 0.78 |
loading | 加载 | 0 | 0 |
other stuff | 未知 | 0.06 | 0.11 |
咱们来看看前3帧里面两个框架都发生了什么:
jQuery:
GSAP:
看来GSAP比起jQuery主要的性能优化在下面这两个类目:
GSAP的渲染详情内容:
jQuery的渲染详情内容:
这样看来,timer形成了很大的区别,而渲染部分本应没有太大区别(layout因为动画部分是position:absolute
,影响范围不大),可是两者的最终差别也比较大,咱们只有经过源码和用例看到区别了。
先看看测试页面jQuery和GSAP的用例:
//jQuery jQuery.easing.cubicIn = $.easing.cubicIn = function( p, n, firstNum, diff ) { //we need to add the standard CubicIn ease to jQuery return firstNum + p * p * p * diff; } jQuery.fx.interval = 10; //ensures that jQuery refreshes at roughly 100fps like GSAP, TweenJS, and most of the others to be more even/fair. tests.jquery = { milliseconds:true, wrapDot:function(dot) { return jQuery(dot); //wrap the dot in a jQuery object in order to perform better (that way, we don't need to query the dom each time we tween - we can just call animate() directly on the jQuery object) }, 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) }); }, stop:function(dot) { dot.stop(true); }, nativeSize:false }; //GSAP (TweenLite) top/left/width/height tests.gsap = { milliseconds:false, wrapDot:function(dot) { return dot; //no wrapping necessary }, tween:function(dot) { var angle = Math.random() * Math.PI * 2; dot.style.cssText = startingCSS; TweenLite.to(dot, duration, {css:{left:Math.cos(angle) * radius + centerX, top:Math.sin(angle) * radius + centerY, width:32, height:32}, delay:Math.random() * duration, ease:Cubic.easeIn, overwrite:"none", onComplete:tests.gsap.tween, onCompleteParams:[dot]}); }, stop:function(dot) { TweenLite.killTweensOf(dot); }, nativeSize:false }; function toggleTest() { inProgress = !inProgress; var i; if (inProgress) { currentTest = tests[engineInput.value]; size = (currentTest.nativeSize ? "16px" : "1px"); centerX = jQuery(window).width() / 2; centerY = (jQuery(window).height() / 2) - 30; startingCSS = "position:absolute; left:" + centerX + "px; top:" + centerY + "px; width:" + size + "; height:" + size + ";"; radius = Math.sqrt(centerX * centerX + centerY * centerY); duration = Number(durInput.value); createDots(); i = dots.length; while (--i > -1) { currentTest.tween(dots[i]); } } }
jQuery部分除了时间函数"CubicIn"咱们日常用不上之外,其余的部分都符合咱们的正常使用习惯。注意到jQuery的jQuery.fx.interval
,也就是MsPF(millisecond per frame,我编的单位)被调到了10,换言之,FPS是100。
“以让测试更加公平”,注释说。
雪姨:“好大的口气”
让咱们接着看源码……
以前的观测结果代表:JS部分中,主要是timer:jQuery里面,每帧大概有10~20个timer被触发,并维持在67ms左右;GSAP每帧不超过6个timer,同时每帧短于30ms。
我在本身的一个空白页面引用了jQuery的1.10.2的未压缩版,而后用chrome打开页面,并在console输入jQuery.Animation
,回车,查看它的定义。并一步步查看其中我以为有可能会带我到定时器的函数定义,直到获得结果。
在这个过程当中,我知道了,jQuery的Animation采用的定时器是setInterval:
jQuery.Animation = function Animation( elem, properties, options ) { // 上部分省略... jQuery.fx.timer( jQuery.extend( tick, { elem: elem, anim: animation, queue: animation.opts.queue }) ); // attach callbacks from options return animation.progress( animation.opts.progress ) .done( animation.opts.done, animation.opts.complete ) .fail( animation.opts.fail ) .always( animation.opts.always ); } jQuery.fx.timer = function ( timer ) { if ( timer() && jQuery.timers.push( timer ) ) { jQuery.fx.start(); } } jQuery.fx.start = function () { if ( !timerId ) { timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); } } jQuery.fx.interval = 13;
就算咱们不在这个项目里调节jQuery.fx.interval
到10,原生的jQuery.fx.interval
竟然是一13ms/frame,换算成FPS就是77,要知道有一些浏览器的绘制上限是60FPS,即1000ms ÷ 60frame ≈ 16.7 ms/frame
,这个interval会要求一些浏览器在绘制上限内执行1.3次,浏览器每隔几帧会丢弃掉其中的1次,而这就形成了额外的损耗,这也是在上面的现象中jQuery里面timer过度耗时,被唤起的次数在20次左右的缘由。
而GSAP没有猜错的话,应该是用到requestAnimationFrame
(以及低版本IE下的setTimeout
做为polyfill),并尽量剪短定时器内部内容(jQuery处于兼容性考虑,会作大量条件判断,这方面天然会败给GSAP),来压榨定时器性能的。
咱们在源码中搜requestAnimationFrame
,在TweenLite.js
中:
/* Ticker */ var _reqAnimFrame = window.requestAnimationFrame, _cancelAnimFrame = window.cancelAnimationFrame, _getTime = Date.now || function() {return new Date().getTime();}, _lastUpdate = _getTime(); //now try to determine the requestAnimationFrame and cancelAnimationFrame functions and if none are found, we'll use a setTimeout()/clearTimeout() polyfill. a = ["ms","moz","webkit","o"]; i = a.length; while (--i > -1 && !_reqAnimFrame) { _reqAnimFrame = window[a[i] + "RequestAnimationFrame"]; _cancelAnimFrame = window[a[i] + "CancelAnimationFrame"] || window[a[i] + "CancelRequestAnimationFrame"]; } _class("Ticker", function(fps, useRAF) { var _self = this, _startTime = _getTime(), _useRAF = (useRAF !== false && _reqAnimFrame), _fps, _req, _id, _gap, _nextTime, _tick = function(manual) { _lastUpdate = _getTime(); _self.time = (_lastUpdate - _startTime) / 1000; var overlap = _self.time - _nextTime, dispatch; if (!_fps || overlap > 0 || manual === true) { _self.frame++; _nextTime += overlap + (overlap >= _gap ? 0.004 : _gap - overlap); dispatch = true; } if (manual !== true) { //make sure the request is made before we dispatch the "tick" event so that //timing is maintained. //Otherwise, if processing the "tick" requires a bunch of time (like 15ms) //and we're using a setTimeout() that's based on 16.7ms, //it'd technically take 31.7ms between frames otherwise. _id = _req(_tick); } if (dispatch) { _self.dispatchEvent("tick"); } }; // ... _self.wake = function() { if (_id !== null) { _self.sleep(); } _req = (_fps === 0) ? _emptyFunc : (!_useRAF || !_reqAnimFrame) ? function(f) { return setTimeout(f, ((_nextTime - _self.time) * 1000 + 1) | 0); } : _reqAnimFrame; if (_self === _ticker) { _tickerActive = true; } _tick(2); }; }
这段代码是很是典型的requestAnimationFrame的polyfill。而且在polyfill部分,计算了浏览器的绘制上限的时间间隔,也符合我以前的猜想。
以前的观测结果代表,渲染部分,jQuery没有layout步骤,可是GSAP有,并且每次都影响到整个文档;jQuery的recalculate步骤,每次仅影响1个元素;而GSAP每次影响到170左右的元素。
从观测结果来看,GSAP作的是化零为整,一次性从新布局所有元素的活儿(一次性改变它们的top、left值,甚至有多是从新替换了一整个DOM内部的所有HTML)。
是这样吗?GSAP的源码太过庞大,咱们怎么构造对代码结构的感性认识呢?
我把官方用例保存到了本地,用的是xxx.htm,这样会生成一个xxx_files文件夹,里面有全部引用的资源文件。(颇有意思,png没有保存下来)。而后我用没有压缩过的源代码文件替代了TweenLite.min.js
和CSSPlugin.min.js
。
如今我再生成一次timeline:
这个时候触发Recalculate style的代码行数与调用栈很是清晰了。我点进p.setRatio@CSSPlugin.min.js:2066
,在当前行新建一个断点,而后刷新页面:
调用栈与上下文都出现了,这个时候的源码是清晰可读的。上下文是没有innerHTML或者$.html之类的代码,我在这里知道,没有采用一次性刷新innerHTML的方法(事实上,这样作的代价也很高)。这里每一步改变的都是CSSStyleDeclaration
,但这个CSSStyleDeclaration
没有链接到相应元素。
仔细阅读调用栈每一层的上下文以后,我作出了它分层的依据:
调用栈(自顶向下) | 代码理解 |
_tick | 重绘时间管理层,用rAF函数绘制一帧 |
EventDispatcher.dispatchEvent | 事件处理层,用事件代替回调 |
Animation._updateRoot | 动画管理层,在这里还会每隔必定帧数作一次gc |
SimpleTimeline.render | 时间线管理层,链式帧的结构 |
TweenLite.render | 帧管理层,本帧和下一帧的引用,计算Tween值 |
CSSPlugin.setRatio | CSS样式管理层,处理CSS样式最终的格式 |
对jQuery的测试用例作一样的事情,能够看到,在最底端,也就是触发recalculate的一端,style直接引用的是DOM元素的style。
jQuery.extend.style = function( elem, name, value, extra ) { var ret, type, hooks, origName = jQuery.camelCase( name ), style = elem.style; name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; if ( value !== undefined ) { //style里面的一大堆判断省略 style[ name ] = value; } }
画出相应的架构:
调用栈(自顶向下) | 代码理解 |
jQuery.dequeue.next | jQuery队列函数 |
jQuery.fn.animate | Animate函数,这里创建Animation对象 |
jQuery.fx.timer | 定时器管理,在这里缓存全部的定时器 |
jQuery.fx.start | 使用setInterval开始一个定时器 |
Animation.tick | Animation对象,管理帧和tween值(中间值)的关系 |
jQuery.Tween.run | Tween对象,处理中间值和时间函数的关系 |
jQuery.Tween.propHooks.set | 抽象set函数,以set各类prop |
jQuery.style | set函数的实例化,处理元素的style |
意识到了吗,jQuery是过程化的,每一个函数/类表明一个须要管理/控制兼容性的需求。
综上所述,咱们获得如下假设:
setInterval
,受到浏览器重绘上限的控制,而GSAP采用requestAnimationFrame
,彻底将重绘交给浏览器管理,以得到更好地重绘性能是这样的吗?
请看下文: 求索:GSAP的动画快于jQuery吗?为什么?/续