製做60fps的高性能動畫

寫在前面

說到web的高性能動畫,這部份內容其實已是老生常談的了,不過其中仍是有很多比較新的並且很是實用的內容能夠和你們分享一下。 讀完這篇文章後相信你們都會對動畫渲染的機制以及製做60fps動畫的關鍵要素有足夠的理解,之後趕上了動畫相關的問題也能夠很好的從源頭上解決。javascript

正文

什麼是高性能動畫呢?

動畫幀率能夠做爲衡量標準,通常來講畫面在 60fps 的幀率下效果比較好。 css

fps對比:對咱們的眼睛來講30fps感受流暢,60fps更舒服完美
60fps

換算一下就是,每一幀要在 16.7ms (16.7 = 1000/60) 內完成渲染。所以,咱們的首要任務是減小沒必要要的性能消耗。 越多的幀須要渲染的,意味着有越多的任務須要瀏覽器處理,因此掉幀就出現了,這是達到 60fps 的一個絆腳石。html

若是全部動畫都沒法在 16.7ms 渲染完畢,不如考慮用略低的 30fps 幀率來渲染。html5

如何實現絲般順滑

這裏主要決定因素有二:java

時機(Frame Timing): 新的一幀準備好的時機git

成本(Frame Budget): 渲染新的一幀須要多長的時間github

開始繪製的時機

通常來講咱們使用setTimeout(callback, 1/60)來實現16.7ms後執行動畫一幀的渲染。 然而setTimeout實際上並不許確。 首先,setTimeout依靠瀏覽器內置時鐘的更新頻率 例如:IE8及之前更新間隔爲15.6ms,setTimeout(callback, 1/60)爲16.7ms,那麼它就須要兩個15.6ms纔會觸發,這也意味着無端延遲了 15.6 x 2 - 16.7 = 14.5毫秒。 web

時鐘頻率
t01457cd049f86cc7ef

其次,假使可以達到16.7ms,它還要面臨一個異步隊列的問題。 由於異步的關係setTimeout中的回調函數並不是當即執行,而是須要加入等待隊列中。但問題是,若是在等待延遲觸發的過程當中,有新的同步腳本須要執行,那麼同步腳本不會排在timer的回調以後,而是當即執行。瀏覽器

function runForSeconds(s) {
    var start = +new Date();
    while (start + s * 1000 > (+new Date())) {}
}

document.body.addEventListener("click", function () {
    runForSeconds(10);
}, false);

setTimeout(function () {
    console.log("Done!");
}, 1000 * 3);
複製代碼

以上的例子是,若是在等待觸發延遲的3秒過程當中,有人點擊了body,那麼回調仍是準時在3s完成時觸發嗎? 實踐執行的時候,它會等待10s,同步函數老是優先於異步函數。性能優化

基於這些問題咱們提出了另外一個解決方案:requestAnimationFrame(callback)

window.requestAnimationFrame() 方法告訴瀏覽器您但願執行動畫並請求瀏覽器在下一次重繪以前調用指定的函數來更新動畫。該方法使用一個回調函數做爲參數,這個回調函數會在瀏覽器重繪以前調用。-- MDN

當咱們調用這個函數的時候,咱們告訴它須要作兩件事:

  1. 咱們須要新的一幀;
  2. 當你渲染新的一幀時須要執行我傳給你的回調函數

與 setTimeout 相比,rAF(requestAnimationFrame) 最大的優點是由系統來決定回調函數的執行時機

具體一點講就是,系統每次繪製以前會主動調用 rAF 中的回調函數,若是系統繪製率是 60Hz,那麼回調函數就每16.7ms 被執行一次,若是繪製頻率是75Hz,那麼這個間隔時間就變成了 1000/75=13.3ms。

換句話說就是,rAF 的執行步伐跟着系統的繪製頻率走。它能保證回調函數在屏幕每一次的繪製間隔中只被執行一次(函數節流,這篇文章就不細說了,感興趣的能夠查一下),這樣就不會引發丟幀現象,也不會致使動畫出現卡頓的問題。

另外它能夠自動調節頻率。若是callback工做太多沒法在一幀內完成會自動下降爲30fps。雖然下降了,但總比掉幀好。

同時對比使用 setTimeout 實現的動畫,當頁面被隱藏或最小化時,setTimeout 仍然在後臺執行動畫任務,因爲此時頁面處於不可見或不可用狀態,刷新動畫是沒有意義的,並且還浪費 CPU 資源。而 rAF 則徹底不一樣,當頁面處理未激活的狀態下,該頁面的屏幕繪製任務也會被系統暫停,所以跟着系統步伐走的 rAF 也會中止渲染,當頁面被激活時,動畫就從上次停留的地方繼續執行,有效節省了 CPU 開銷。


對於rAF的兼容性問題其實已經有了很好的處理方案了,如下是一種比較簡單的:

window.requestAnimFrame = (function(){
 return  window.requestAnimationFrame   || 
   window.webkitRequestAnimationFrame || 
   window.mozRequestAnimationFrame    || 
   window.oRequestAnimationFrame      || 
   window.msRequestAnimationFrame     || 
   function( callback ){
        window.setTimeout(callback, 1000 / 60);
   };
})();
複製代碼

這種寫法沒有考慮 cancelAnimationFrame 的兼容性,而且不是全部的設備繪製時間間隔都是1000/60。

這個是比較不錯的polyfil

繪製一幀的時間

總的來講,rAF解決了前面的第一個問題(繪製時機),至於第二個問題(繪製成本),rAF是無能爲力的,最多也就是採起自動下降頻率的方式處理。

這裏就須要從瀏覽器渲染方面來優化了,首先看下這個圖:

渲染過程
t01018eff532098fece

Rendering

頁面首次加載時,瀏覽器會下載並解析 HTML,將 HTML 元素轉變爲一個 DOM 節點的「內容樹」(content tree)。除此以外,樣式一樣會被解析生成「渲染樹」 (render tree)。爲了提高性能,渲染引擎會分開完成這些工做,甚至會出現渲染樹比 DOM 樹更快生成出來。

在這個階段裏最影響繪製時間的天然就是Layout了

// animation loop
function update(timestamp) {
    for(var m = 0; m < movers.length; m++) {
        // DEMO 版本
        //movers[m].style.left = ((Math.sin(movers[m].offsetTop + timestamp/1000)+1) * 500) + 'px';

        // FIXED 版本
        movers[m].style.left = ((Math.sin(m + timestamp/1000)+1) * 500) + 'px';
        }
    rAF(update);
};
rAF(update);
複製代碼

上面例子裏DEMO版本是很是慢的,之因此慢的緣由是,在修改每個物體的left值時,會請求這個物體的offsetTop值,觸發了重排,這是一個很是耗時的reflow操做。

一般咱們會不知不覺中寫了不少的頻繁layout的代碼,例如:

var h1 = element1.clientHeight;
element1.style.height = (h1 * 2) + 'px';

var h2 = element2.clientHeight; 
element2.style.height = (h2 * 2) + 'px';

var h3 = element3.clientHeight;
element3.style.height = (h3 * 2) + 'px';
複製代碼

不斷地讀寫 DOM 會致使「強制同步佈局」(forced synchronous layouts),不過在技術發展過程當中它演變成了更形象的詞 — 「佈局抖動」(layout thrashing)(詳情能夠看一下這篇文章 layout thrashing)。 瀏覽器會追蹤「髒元素」,在合適的時候將變換過程儲存起來,而後在讀取了特定屬性之後,開發者能夠強制瀏覽器提早計算,這樣反覆的讀寫會致使重排。 因此這裏咱們須要進行優化,先讀後寫就是一個解決方案,上面的代碼能夠改寫爲:

// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Write
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
複製代碼

固然這種只能應對一些普通的狀況,若是代碼是解耦的或者更復雜的讀寫後嵌套讀寫操做的這些狀況可使用一些比較成熟的解決方案,例如fastdom.js。另一個小技巧是使用rAF來延遲所有的寫操做到下一幀執行也是很不錯的解決方案。

Paint

生成佈局後,瀏覽器將頁面繪製到屏幕上。這個環節和前一個步驟相似,瀏覽器會追蹤髒元素,將它們合併到一個超大的矩形區域中。每一幀內只會發生一次重繪,用於繪製這個被污染區域。

這個階段對性能的影響主要在於重繪。

減小沒必要的繪製

例如,gif圖即便不可見,也可能致使paint,不須要時應將gif圖的display屬性設爲none 在常常paint的區域,要避免代價過高的style 代價比較高的樣式:

color,border-style,visibility,background,
text-decoration,background-image,
background-position,background-repeat
outline-color,outline,outline-style
border-radius,outline-width,box-shadow
background-size
複製代碼

參考網站:csstriggers.com/

減小繪製的區域

爲引發大範圍Paint的元素生成獨立的Layer以減少Paint的範圍

能夠參考一下這個demo網站,綠色部分爲重繪區域:

demo網站截圖

Composite

將全部繪製好的元素進行復合。 默認狀況下,全部元素將會被繪製到同一個層中,若是將元素分開到不一樣的複合層中,更新元素對性能友好,不在同一層的元素不容易受到影響。

這一階段裏CPU 繪製層,GPU 生成層。GPU 複合層上的改變代價最小性能消耗最少。因此這裏的優化主要就是把代價高的改動都放到GPU上,也就是通常說的開啓硬件加速技術,能夠說有益無害,若是設備的性能足夠開啓就對了。

這裏的限制主要有:GPC和CPU之間帶寬,GPU的限度。


這裏須要區分一下CPU,GPU的工做:

enter description here
t01918abbc87b4f13ba
CPU工做比較多,還分主線程和合成線程。 主線程主要負責:

  1. Javascript 的計算與執行
  2. CSS 樣式計算
  3. Layout 計算
  4. 將頁面元素繪製成位圖(paint),也就是光柵化(Raster)
  5. 將位圖給合成線程

合成線程則主要負責:

  1. 將位圖(GraphicsLayer 層)以紋理(texture) 的形式上傳給 GPU(GPC和CPU之間帶寬)
  2. 計算頁面的可見部分和即將可見部分(滾動)
  3. CSS 動畫處理(CSS 動畫而言,因爲其流程不受主線程的影響,因此性能更好。)
  4. 通知 GPU 繪製位圖到屏幕上

而GPU就只須要繪製圖層了,因此硬件加速的性能無疑更好。


開啓硬件加速的方式主要有:

  1. 經過改變 opacitytransform 的值觸發
  2. 經過transform的3D屬性強制開啓GPU加速
  3. will-change顯式地通知瀏覽器對某一個元素的某個或某些元素作渲染優化

硬件加速以後,瀏覽器會爲此元素單首創建一個「層」。當有單獨的層以後,此元素的repaint操做將只須要更新本身,不用影響到別人。你能夠將其理解爲局部更新。因此開啓了硬件加速的動畫會變得流暢不少

默認狀況下,transformopacity這類css屬性CPU是直接通知GPU來作處理的,由於GPU能快速對texture(紋理:CPU傳輸到GPU的一個Bitmap)進行偏移、縮放、旋轉、修改透明度等操做,不通過主線程的layout、paint過程。也就是開啓了硬件加速。

will-change是個新事物,它可以顯式地通知瀏覽器對某一個元素的某個或某些元素作渲染優化。 will-change 接收各類各樣的屬性值,好比一個或多個 CSS 屬性 (transform, opacity)、contents 或者 scroll-position。不過最經常使用值可能就是 auto,這個值表示的是瀏覽器將進行默認的優化:


GPU雖然擅長處理圖像,可是它也有瓶頸。

鏈接CPU和GPU之間的帶寬是有限的,若是一次更新的層太多,則很容易就達到GPU的瓶頸,影響到動畫的流暢度。因此咱們須要控制層的數量和層paint的次數。

控制層的數量能夠理解,由於層的建立和更新都會消耗內存。而控制層paint的次數,是爲了減小位圖更新的次數。每次位圖更新,合成線程就須要提交新的位圖給GPU。頻繁地更新位圖也會拖慢GPU的效率。

優化有度,咱們總能聽到關於「複合層過多反而阻礙渲染」的討論。由於瀏覽器已經爲優化作了能作的一切, will-change 的性能優化方案自己對資源要求很高。若是瀏覽器持續在執行某個元素的 will-change,就意味着瀏覽器要持續對這個元素的進行優化,性能消耗形成頁面卡頓。

過多的複合層下降頁面性能的現象在移動端很常見。


避免意外生成的layer

z-index高於Layer的元素,也會生成單獨的Layer

demo以及說明頁面

小結

實現絲般順滑主要決定因素有二:

時機(Frame Timing):

  • rAF

成本(Frame Budget):

  • 避免layout:先讀後寫

  • 儘可能少paint:注意樣式的使用

  • 適當的硬件加速

相關文章
相關標籤/搜索