本文已完結,請看下文: > 求索: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嗎?爲什麼?/續