摘要:本文已完結(jié),請(qǐng)看下文求索的動(dòng)畫快于嗎為何續(xù)本文源自對(duì)問題動(dòng)畫性能優(yōu)于的原理是什么的回答。是這樣的嗎請(qǐng)看下文求索的動(dòng)畫快于嗎為何續(xù)
本文已完結(jié),請(qǐng)看下文: > 求索:GSAP的動(dòng)畫快于jQuery嗎?為何?/續(xù)
本文源自對(duì)問題《GSAP js動(dòng)畫性能優(yōu)于jQuery的原理是什么?》的回答。GSAP是一個(gè)js動(dòng)畫插件,它聲稱“20x faster than jQuery”,是什么讓它這么快呢?
每當(dāng)有這樣的問題的時(shí)候,我們可以通過以下步驟來確定一個(gè)未知的解決方案的性能優(yōu)化是怎么做到/偽造的:
黑盒:從官方用例來看,究竟有多快,快在哪兒
白盒:看看官方用例之內(nèi),框架怎么做到優(yōu)化的
do { 提出假設(shè),自己構(gòu)建用例測(cè)試 } while (假設(shè)沒有得到驗(yàn)證);
得出結(jié)論
文中提到的timer、recalculate、layout、repaint、composite layer,需要瀏覽器內(nèi)部運(yùn)行相關(guān)的基礎(chǔ)知識(shí)。見:
《瀏覽器的渲染原理簡介》
《瀏覽器的工作原理:新式網(wǎng)絡(luò)瀏覽器幕后揭秘》
黑盒:從用例來看,究竟有多快,快在哪兒首先我們打開chrome,并開啟官網(wǎng)的H5動(dòng)畫速度測(cè)試頁面:http://www.greensock.com/js/speed.html。
頁面中用js計(jì)算出的fps很不準(zhǔn)確,還是以瀏覽器的統(tǒng)計(jì)為準(zhǔn)。
在jQuery和GSAP兩個(gè)框架下打開,然后點(diǎn)run,然后f12審查元素,進(jìn)入Timeline頁面,點(diǎn)record。過了100frame以后暫停,然后進(jìn)入頁面點(diǎn)擊stop。
以下是jQuery的結(jié)果:100幀6.53s,平均FPS:15幀/秒
(也可以自己算出來 100frames ÷ 6.53s ≈ 15.3FPS)
以下是GSAP的結(jié)果:100幀2.22s,平均FPS:45幀/秒。比jQuery快2倍呢。
來對(duì)比一下100幀里面各個(gè)流程的耗時(shí)(單位:秒):
類目 | 詳情 | jQuery | GSAP |
scripting | timer等js執(zhí)行 | 2.87 | 0.52 |
rendering | recalculate(重計(jì)算)、layout(回流) | 2.04 | 0.77 |
painting | repaint(重繪)、composite layers(混合圖層) | 0.88 | 0.78 |
loading | 加載 | 0 | 0 |
other stuff | 未知 | 0.06 | 0.11 |
我們來看看前3幀里面兩個(gè)框架都發(fā)生了什么:
jQuery:
GSAP:
看來GSAP比起jQuery主要的性能優(yōu)化在下面這兩個(gè)類目:
JS:主要是timer,jQuery里面,每幀大概有10~20個(gè)timer被觸發(fā)(并維持在67ms左右);GSAP每幀不超過6個(gè)timer(并短于30ms);
渲染:jQuery沒有l(wèi)ayout步驟,但是GSAP有,而且每次都影響到整個(gè)文檔;jQuery的recalculate步驟,每次僅影響1個(gè)元素;而GSAP每次影響到170左右的元素。
GSAP的渲染詳情內(nèi)容:
jQuery的渲染詳情內(nèi)容:
這樣看來,timer造成了很大的區(qū)別,而渲染部分本應(yīng)沒有太大區(qū)別(layout由于動(dòng)畫部分是position:absolute,影響范圍不大),但是二者的最終差異也比較大,我們只有通過源碼和用例看到區(qū)別了。
白盒:看看用例之內(nèi),GSAP框架怎么做到優(yōu)化的 帶著懷疑看看用例先看看測(cè)試頁面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部分除了時(shí)間函數(shù)"CubicIn"我們平常用不上以外,其他的部分都符合我們的正常使用習(xí)慣。注意到j(luò)Query的jQuery.fx.interval,也就是MsPF(millisecond per frame,我編的單位)被調(diào)到了10,換言之,F(xiàn)PS是100。
“以讓測(cè)試更加公平”,注釋說。
雪姨:“好大的口氣”
讓我們接著看源碼……
JS運(yùn)行優(yōu)化之前的觀測(cè)結(jié)果表明:JS部分中,主要是timer:jQuery里面,每幀大概有10~20個(gè)timer被觸發(fā),并維持在67ms左右;GSAP每幀不超過6個(gè)timer,同時(shí)每幀短于30ms。
我在自己的一個(gè)空白頁面引用了jQuery的1.10.2的未壓縮版,然后用chrome打開頁面,并在console輸入jQuery.Animation,回車,查看它的定義。并一步步查看其中我覺得有可能會(huì)帶我到定時(shí)器的函數(shù)定義,直到得到結(jié)果。
在這個(gè)過程中,我知道了,jQuery的Animation采用的定時(shí)器是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;
就算我們不在這個(gè)項(xiàng)目里調(diào)節(jié)jQuery.fx.interval到10,原生的jQuery.fx.interval居然是一13ms/frame,換算成FPS就是77,要知道有一些瀏覽器的繪制上限是60FPS,即1000ms ÷ 60frame ≈ 16.7 ms/frame,這個(gè)interval會(huì)要求一些瀏覽器在繪制上限內(nèi)執(zhí)行1.3次,瀏覽器每隔幾幀會(huì)丟棄掉其中的1次,而這就造成了額外的損耗,這也是在上面的現(xiàn)象中jQuery里面timer過分耗時(shí),被喚起的次數(shù)在20次左右的原因。
而GSAP沒有猜錯(cuò)的話,應(yīng)該是用到requestAnimationFrame(以及低版本IE下的setTimeout作為polyfill),并盡可能剪短定時(shí)器內(nèi)部內(nèi)容(jQuery處于兼容性考慮,會(huì)做大量條件判斷,這方面自然會(huì)敗給GSAP),來壓榨定時(shí)器性能的。
我們?cè)谠创a中搜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部分,計(jì)算了瀏覽器的繪制上限的時(shí)間間隔,也符合我之前的猜測(cè)。
渲染優(yōu)化之前的觀測(cè)結(jié)果表明,渲染部分,jQuery沒有l(wèi)ayout步驟,但是GSAP有,而且每次都影響到整個(gè)文檔;jQuery的recalculate步驟,每次僅影響1個(gè)元素;而GSAP每次影響到170左右的元素。
從觀測(cè)結(jié)果來看,GSAP做的是化零為整,一次性重新布局全部元素的活兒(一次性改變它們的top、left值,甚至有可能是重新替換了一整個(gè)DOM內(nèi)部的全部HTML)。
是這樣嗎?GSAP的源碼太過龐大,我們?cè)趺礃?gòu)造對(duì)代碼結(jié)構(gòu)的感性認(rèn)識(shí)呢?
我把官方用例保存到了本地,用的是xxx.htm,這樣會(huì)生成一個(gè)xxx_files文件夾,里面有所有引用的資源文件。(很有意思,png沒有保存下來)。然后我用沒有壓縮過的源代碼文件替代了TweenLite.min.js和CSSPlugin.min.js。
現(xiàn)在我再生成一次timeline:
這個(gè)時(shí)候觸發(fā)Recalculate style的代碼行數(shù)與調(diào)用棧非常清晰了。我點(diǎn)進(jìn)p.setRatio@CSSPlugin.min.js:2066,在當(dāng)前行新建一個(gè)斷點(diǎn),然后刷新頁面:
調(diào)用棧與上下文都出現(xiàn)了,這個(gè)時(shí)候的源碼是清晰可讀的。上下文是沒有innerHTML或者$.html之類的代碼,我在這里知道,沒有采用一次性刷新innerHTML的方法(事實(shí)上,這樣做的代價(jià)也很高)。這里每一步改變的都是CSSStyleDeclaration,但這個(gè)CSSStyleDeclaration沒有連接到相應(yīng)元素。
仔細(xì)閱讀調(diào)用棧每一層的上下文之后,我做出了它分層的依據(jù):
調(diào)用棧(自頂向下) | 代碼理解 |
_tick | 重繪時(shí)間管理層,用rAF函數(shù)繪制一幀 |
EventDispatcher.dispatchEvent | 事件處理層,用事件代替回調(diào) |
Animation._updateRoot | 動(dòng)畫管理層,在這里還會(huì)每隔一定幀數(shù)做一次gc |
SimpleTimeline.render | 時(shí)間線管理層,鏈?zhǔn)綆慕Y(jié)構(gòu) |
TweenLite.render | 幀管理層,本幀和下一幀的引用,計(jì)算Tween值 |
CSSPlugin.setRatio | CSS樣式管理層,處理CSS樣式最終的格式 |
對(duì)jQuery的測(cè)試用例做同樣的事情,可以看到,在最底端,也就是觸發(fā)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; } }
畫出相應(yīng)的架構(gòu):
調(diào)用棧(自頂向下) | 代碼理解 |
jQuery.dequeue.next | jQuery隊(duì)列函數(shù) |
jQuery.fn.animate | Animate函數(shù),這里建立Animation對(duì)象 |
jQuery.fx.timer | 定時(shí)器管理,在這里緩存所有的定時(shí)器 |
jQuery.fx.start | 使用setInterval開始一個(gè)定時(shí)器 |
Animation.tick | Animation對(duì)象,管理幀和tween值(中間值)的關(guān)系 |
jQuery.Tween.run | Tween對(duì)象,處理中間值和時(shí)間函數(shù)的關(guān)系 |
jQuery.Tween.propHooks.set | 抽象set函數(shù),以set各種prop |
jQuery.style | set函數(shù)的實(shí)例化,處理元素的style |
意識(shí)到了嗎,jQuery是過程化的,每個(gè)函數(shù)/類代表一個(gè)需要管理/控制兼容性的需求。
假設(shè)與驗(yàn)證,自己構(gòu)建測(cè)試用例,看看優(yōu)化是否名不副實(shí)綜上所述,我們得到以下假設(shè):
jQuery的定時(shí)器采用的是setInterval,受到瀏覽器重繪上限的控制,而GSAP采用requestAnimationFrame,完全將重繪交給瀏覽器管理,以獲得更好地重繪性能
jQuery每次都是多帶帶修改一個(gè)DOM的style,而GSAP是計(jì)算離線的style,然后再賦給DOM。這導(dǎo)致了不一樣的時(shí)間開銷。
jQuery沒有集中繪制,每個(gè)DOM都在一個(gè)事件回調(diào)函數(shù)上下文中處理,有多少個(gè)DOM就有多少個(gè)上下文;GSAP有集中繪制。同時(shí)jQuery是過程化的,GSAP是面向?qū)ο蟮摹_@讓jQuery非常難以做到集中控制繪制。jQuery會(huì)將DOM的引用一路傳遞到最終改變DOM的style函數(shù)中,這在調(diào)用過程中也會(huì)非常浪費(fèi)空間。這些也都導(dǎo)致了不一樣的時(shí)間開銷。
是這樣的嗎?
請(qǐng)看下文: 求索:GSAP的動(dòng)畫快于jQuery嗎?為何?/續(xù)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/110843.html
摘要:本文是求索的動(dòng)畫快于嗎為何的續(xù)文。沒有集中繪制,每個(gè)都在一個(gè)事件回調(diào)函數(shù)上下文中處理,有多少個(gè)就有多少個(gè)上下文有集中繪制。測(cè)試過程中為了比較好的效果用了隨機(jī)數(shù)。 本文是求索:GSAP的動(dòng)畫快于jQuery嗎?為何? 的續(xù)文。GSAP是一個(gè)js動(dòng)畫插件,它聲稱20x faster than jQuery,是什么讓它這么快呢? 每當(dāng)有這樣的問題的時(shí)候,我們可以通過以下步驟來...
摘要:現(xiàn)在又多了一種實(shí)現(xiàn)動(dòng)畫的方案,那就是還在草案當(dāng)中的方法。這個(gè)方法就是傳遞給的回調(diào)函數(shù)。為回調(diào)函數(shù)一個(gè)簡單的例子模擬一個(gè)進(jìn)度條動(dòng)畫,初始寬度為在函數(shù)中將進(jìn)度加然后再更新到寬度上,在進(jìn)度達(dá)到之前,一直重復(fù)這一過程。 HTML5/CSS3時(shí)代,我們要在web里做動(dòng)畫選擇其實(shí)已經(jīng)很多了: 你可以用CSS3的animattion+keyframes; 你也可以用css3的transition...
摘要:現(xiàn)在又多了一種實(shí)現(xiàn)動(dòng)畫的方案,那就是還在草案當(dāng)中的方法。這個(gè)方法就是傳遞給的回調(diào)函數(shù)。為回調(diào)函數(shù)一個(gè)簡單的例子模擬一個(gè)進(jìn)度條動(dòng)畫,初始寬度為在函數(shù)中將進(jìn)度加然后再更新到寬度上,在進(jìn)度達(dá)到之前,一直重復(fù)這一過程。 HTML5/CSS3時(shí)代,我們要在web里做動(dòng)畫選擇其實(shí)已經(jīng)很多了: 你可以用CSS3的animattion+keyframes; 你也可以用css3的transition...
摘要:現(xiàn)在又多了一種實(shí)現(xiàn)動(dòng)畫的方案,那就是還在草案當(dāng)中的方法。這個(gè)方法就是傳遞給的回調(diào)函數(shù)。為回調(diào)函數(shù)一個(gè)簡單的例子模擬一個(gè)進(jìn)度條動(dòng)畫,初始寬度為在函數(shù)中將進(jìn)度加然后再更新到寬度上,在進(jìn)度達(dá)到之前,一直重復(fù)這一過程。 HTML5/CSS3時(shí)代,我們要在web里做動(dòng)畫選擇其實(shí)已經(jīng)很多了: 你可以用CSS3的animattion+keyframes; 你也可以用css3的transition...
摘要:雖然沒有視覺效果,但這就是基本的值動(dòng)畫。有專門的位置可以查詢緩動(dòng)函數(shù)。另外,不要期望在不支持的瀏覽器上做動(dòng)畫。是專業(yè)動(dòng)畫庫,在大部分情況下,它也具備更好的動(dòng)畫性能。 說到在網(wǎng)頁里創(chuàng)建動(dòng)畫,你可能很快會(huì)想到j(luò)Query的animate()方法,或者css3的animation和transition。現(xiàn)在,本文將介紹另一個(gè)web動(dòng)畫的可選方案,GSAP。 GSAP的全名是GreenSock...
閱讀 4152·2023-04-26 02:40
閱讀 2655·2023-04-26 02:31
閱讀 2746·2021-11-15 18:08
閱讀 568·2021-11-12 10:36
閱讀 1425·2021-09-30 09:57
閱讀 5192·2021-09-22 15:31
閱讀 2626·2019-08-30 14:17
閱讀 1269·2019-08-30 12:58