H5動畫優化之路

H5動畫60fps之路

在移動端,和Native相比,H5一直都被人吐槽性能差,尤爲是在動畫方面。
談到整個Web app的生命週期,通常分爲四個部分:javascript

  1. 加載
  2. 等待用戶
  3. 響應用戶
  4. 動畫

通常狀況下,首屏加載的時間應該小於1s,而響應用戶行爲的時間應該小於100ms,動畫應該達到60fps。這篇文章只針對動畫60fps的優化。css

關鍵渲染路徑

動畫性能高,從直觀上來看是動畫沒有抖動和卡頓,從數字上是渲染達到了60fps。60fps就是每秒60幀,因此每幀的時間只有1000ms/60=16.67ms。實際上,瀏覽器在每一幀還要作一些額外的事情,因此若是要達成60fps,咱們須要保證每一幀的時間在10ms到12ms之間。java

咱們先來看看瀏覽器在每一幀渲染都作了哪些事情。chrome

javascript

現階段CSS動畫接口比較有限,許多複雜的動畫若是css不能完成,就須要使用requestAnimationFrame函數,這樣每幀都會有JavaScript的計算。瀏覽器

Style

當一個新的樣式應用到Dom上的時候,就會引發Style的計算,同JavaScript,若是開發者使用CSS Animation,CSS Transition,那麼瀏覽器只有在動畫開始以前會作Sytle的計算,而requireAnimationFrame會在每幀都計算。多線程

Layout

當新的CSS樣式出發了Layout,好比修改了width,height,pisition,這時瀏覽器須要從新Layout受到影響的元素,大部分狀況下,即便一個位置很遠的元素髮生很小的寬度改變,也會引發整個document的layout,這在動畫裏是一個性能很是低的實現,應該儘可能避免。app

Paint

Layout變化後,受到影響的元素會從新Paint,若是遇到首次加載圖片,瀏覽器須要將圖片先解碼放入內存(Image Decode)。Painting是在多個Surface上進行的,最後會出來多個Layer。函數

Composite

最後一步Composite就是把已經Paint好的各個Layer合成到一塊兒。瀏覽器會將每一個層分紅多個Tiles去Paint,可是這些做爲H5開發者來講是不可以控制的,這些層和Tiles信息會被傳到GPU,最終由GPU負責渲染並顯示在屏幕上。工具

三條路

並非每一個CSS的變更引發的渲染過程都要經歷以上所有,實際有三條路能夠走。
第一條路,好比修改元素的width,這些步驟都會執行。
第二條路:修改元素的background-color:不會有佈局的變化,不會觸發Layout,可是會發生Paint
第三條路:修改元素的transform,不會觸發Layout,也不會觸發Paint
當咱們要完成一個動畫時,可能有不少種實現的方式,好比讓一個小球向右平移100px,咱們能夠用如下這些方法:佈局

.move{left:100px};
.move{margin-left:100px};
.move{padding-left:100px};
.move{transform:translateX(100px)};

問題就在這裏,使用哪一種方式性能最高?
從上面看出,若是使用不觸發Layout和Paint的CSS屬性完成動畫,性能是最高的。那麼哪些CSS屬性不會觸發
Layout和Paint呢?
推薦一個網站CSS TRIGGERS,這裏詳細的列出了全部CSS屬性在修改後是否觸發Layout和Paint,是個很是給力的工具!

使用Chrome的DevTools調試

調試動畫就要用到timeline,這裏列出3個比較實用的功能。

  1. 查看每幀的時間:經過錄制跟蹤一段動畫,能夠看到每一幀的狀況,找到耗時超過16ms的幀能夠有針對性地解決問題。
  2. 查看內存:經過Memory工具能夠查看內存的使用狀況,對於內存泄露的檢查是很是有幫助的。
  3. 查看CompositeLayer:經過Layer能夠查看當前繪製的幀有多少須要Composite的Layer,Layer越多,帶來的Upate LayerTree和Composite Layer的時間就越長。

優化方法

使用requestAnimationFrame

前面提到,若是動畫性能達到60fps,那麼每一幀的時間是16ms,瀏覽器會有一些額外的工做,因此要保證全部的事情在10ms到12ms完成,那麼留給JavaScript的時間在3ms到4ms比較合適。

大多數比較早期的Web動畫是用setTimeout/setInterval這兩個函數實現的,包括著名的JQuery,那個年代也沒有其它的API能夠用。這種實現方式性能很是低效,由於JavaScript在處理setTimeout/setInterval時,不會和渲染流程聯繫起來,頗有可能在一幀的中間忽然執行JavaScript致使Sytle和Layout等後面的處理要從新進行而不能在16ms內完成所有。

可是如今不一樣了,咱們有了requestAnimationFrame,使用這個函數,動畫每一幀在什麼時候執行,如何執行,都由瀏覽器去決定,開發者只要把每次執行的工做寫好就能夠了。

function animate(){
        //do something change here,like:
        //x+=0.1;element.style.opacity=x;
        requestAnimationFrame(animate);
    }
    requestAnimationFrame(animate);

查看耗時較長的js,必要時使用WebWorker

若是在動畫執行過程當中,有額外的大量JavaScript計算,爲了避免影響動畫,建議將大量計算的JavaScript放入WebWorker。WebWorker是個功能有限的實現JavaScript多線程的工具,Worker和主線程經過message事件和postMessage方法通信,API能夠參考WebWorker。

micro Optimization

V8引擎已經能夠幫助咱們處理這些細小的優化。可是做爲一種良好的編碼習慣,仍是要保持。

Style優化

瀏覽器花多長時間處理Style的計算取決於有多少元素受到了影響,因此改變1000個元素的Style所耗費的時間,是改變10個元素的100倍。有了這個結論,咱們在作每一個動畫時,要確保每次改變CSS時,所影響的元素都是咱們須要改變的,不要改變和動畫無關的元素。

避免Layput Thrashing

什麼是 Layout Thrashing?先看一個例子:

for(var i=0;i<elements.length;i++){
    var blockWidth=baseElement.Width;
    elements[i].style.width=blockWidth;
}

這是一個性能很低的例子。
當開發者設置了一個CSS樣式,瀏覽器會繼續等待看有沒有更多的樣式改變,而後在接下來的一幀來個批處理。
咱們再看上面那個性能低的例子,設置好一個新的樣式,接着讓瀏覽器去讀一個Layout,這樣瀏覽器就不能等待多個樣式的批處理,而是強制讓它先把剛纔的樣式作渲染,這樣一讀一寫地循環下去,性能是很是低的,叫作「Forced Synchonous Layout」。

這個例子修改起來很是簡單,只要把讀Layout的事情放在循環外就行了。

var blockWidth=baseElement.Width;
for(var i=0;i<elements.length;i++){
    elements[i].style.width=blockWidth;
}

Painting和Composite優化

Painting是很是耗性能的,因此爲了不Painting,咱們通常會把這個元素變成一個Layer,當它成爲一個單獨Layer後,就會獨立出來,當在它後面的其餘元素須要重繪時,它也不受到影響,只是最後須要作Composite合成。可是並非Layer越多越好,這樣會加劇Composite的工做,因此開發者須要在Paint和Composite上找到一個平衡點。

如何讓一個元素變成Layer呢?目前有幾種Hack方式:

.layer{transform:translate3d(0,0,0)}
.layer{transform:translateZ(0)}

Chrome提供了一種更好的方式:使用will-change,和上面不一樣,will-change不會直接把元素變爲Layer,而是給瀏覽器一個提示,這可讓瀏覽器本身決定是否成爲Layer,推薦這種方式:

.layer{will-change:transform;}

推薦一篇Chrome官方的文章GPU Accelerated Compositing in Chrome,這裏面很是詳細地講述了從RenderObject到RenderLayer,到GraphicsLayer,最後變成Chrome Compositor Layers的一系列過程,很是詳細,文章裏提到的GraphicsLayer就是本文的Layer。

上面分別從JavaScript, Style, Paint的過程介紹了一些針對性的優化方法,經過使用Chrome的DevTools,開發者能夠針對渲染路徑上的每個步驟優化,相信必定會有很是好的效果。
我將這些優化點簡單總結爲一個圖:

相關文章
相關標籤/搜索