相信不少人在做移動端動畫的時候遇到了卡頓的問題,這篇文章嘗試從瀏覽器渲染的角度;一點一點告訴你動畫優化的原理及其技巧,作為你工作中優化動畫的參考。文末有優化技巧的總結。
因為GPU合成沒有官方規範,每個瀏覽器的問題和解決方式也不同;所以文章內容僅供參考。
瀏覽器渲染
提高動畫的優化不得不提及瀏覽器是如何渲染一個頁面。在從伺服器中拿到資料後,瀏覽器會先做解析三類東西:
- 解析html,xhtml,svg這三類文件,形成dom樹。
- 解析css,產生css rule tree。
- 解析js,js會通過api來操作dom tree和css rule tree。
解析完成之後,瀏覽器引擎會通過dom tree和css rule tree來構建rendering tree:
- rendering tree和dom tree並不完全相同,例如:<head></head>或display:none的東西就不會放在渲染樹中。
- css rule tree主要是完成匹配,並把css rule附加給rendering tree的每個element。
在渲染樹構建完成後,
- 瀏覽器會對這些元素進行定位和佈局,這一步也叫做reflow或者layout。
- 瀏覽器繪製這些元素的樣式,顏色,背景,大小及邊框等,這一步也叫做repaint。
- 然後瀏覽器會將各層的資訊傳送給GPU,GPU會將各層合成;顯示在螢幕上。
渲染優化原理
如上所說,渲染樹構建完成後;瀏覽器要做的步驟:
reflow——》repaint——》composite
reflow和repaint
reflow和repaint都是耗費瀏覽器效能的操作,這兩者尤以reflow為甚;因為每次reflow,瀏覽器都要重新計算每個元素的形狀和位置。
由於reflow和repaint都是非常消耗效能的,我們的瀏覽器為此做了一些優化。瀏覽器會將reflow和repaint的操作積攢一批,然後做一次reflow。但是有些時候,你的程式碼會強制瀏覽器做多次reflow。例如:
1 2 3 4 |
var content = document.getElementById('content'); content.style.width = 700px; var contentWidth = content.offsetWidth; content.style.backgound = 'red'; |
以上第三行程式碼,需要瀏覽器reflow後;再獲取值,所以會導致瀏覽器多做一次reflow。
下面是一些針對reflow和repaint的最佳實踐:
- 不要一條一條地修改dom的樣式,儘量使用className一次修改。
- 將dom離線後修改
- 使用documentFragment物件在記憶體裡操作dom。
- 先把dom節點display:none;(會觸發一次reflow)。然後做大量的修改後,再把它顯示出來。
- clone一個dom節點在記憶體裡,修改之後;與線上的節點相替換。
- 不要使用table佈局,一個小改動會造成整個table的重新佈局。
- transform和opacity只會引起合成,不會引起佈局和重繪。
從上述的最佳實踐中你可能發現,動畫優化一般都是儘可能地減少reflow、repaint的發生。關於哪些屬性會引起reflow、repaint及composite,你可以在這個網站找到https://csstriggers.com/。
composite
在reflow和repaint之後,瀏覽器會將多個複合層傳入GPU;進行合成工作,那麼合成是如何工作的呢?
假設我們的頁面中有A和B兩個元素,它們有absolute和z-index屬性;瀏覽器會重繪它們,然後將影像傳送給GPU;然後GPU將會把多個影像合成展示在螢幕上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<style> #a, #b { position: absolute; } #a { left: 30px; top: 30px; z-index: 2; } #b { z-index: 1; } </style> <div id="#a">A</div> <div id="#b">B</div> |
我們將A元素使用left屬性,做一個移動動畫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<style> #a, #b { position: absolute; } #a { left: 10px; top: 10px; z-index: 2; animation: move 1s linear; } #b { left: 50px; top: 50px; z-index: 1; } @keyframes move { from { left: 30px; } to { left: 100px; } } </style> <div id="#a">A</div> <div id="#b">B</div> |
在這個例子中,對於動畫的每一幀;瀏覽器會計算元素的幾何形狀,渲染新狀態的影像;並把它們傳送給GPU。(你沒看錯,position也會引起瀏覽器重排的)儘管瀏覽器做了優化,在repaint時,只會repaint部分割槽域;但是我們的動畫仍然不夠流暢。
因為重排和重繪發生在動畫的每一幀,一個有效避免reflow和repaint的方式是我們僅僅畫兩個影像;一個是a元素,一個是b元素及整個頁面;我們將這兩張圖片傳送給GPU,然後動畫發生的時候;只做兩張圖片相對對方的平移。也就是說,僅僅合成快取的圖片將會很快;這也是GPU的優勢——它能非常快地以亞畫素精度地合成圖片,並給動畫帶來平滑的曲線。
為了僅發生composite,我們做動畫的css property必須滿足以下三個條件:
- 不影響文件流。
- 不依賴文件流。
- 不會造成重繪。
滿足以上以上條件的css property只有transform和opacity。你可能以為position也滿足以上條件,但事實不是這樣,舉個例子left屬性可以使用百分比的值,依賴於它的offset parent。還有em、vh等其他單位也依賴於他們的環境。
我們使用translate來代替left
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<style> #a, #b { position: absolute; } #a { left: 10px; top: 10px; z-index: 2; animation: move 1s linear; } #b { left: 50px; top: 50px; z-index: 1; } @keyframes move { from { transform: translateX(0); } to { transform: translateX(70px); } } </style> <div id="#a">A</div> <div id="#b">B</div> |
瀏覽器在動畫執行之前就知道動畫如何開始和結束,因為瀏覽器沒有看到需要reflow和repaint的操作;瀏覽器就會畫兩張影像作為複合層,並將它們傳入GPU。
這樣做有兩個優勢:
- 動畫將會非常流暢
- 動畫不在繫結到CPU,即使js執行大量的工作;動畫依然流暢。
看起來效能問題好像已經解決了?在下文你會看到GPU動畫的一些問題。
GPU是如何合成影像的
GPU實際上可以看作一個獨立的計算機,它有自己的處理器和儲存器及資料處理模型。當瀏覽器向GPU傳送訊息的時候,就像向一個外部裝置傳送訊息。
你可以把瀏覽器向GPU傳送資料的過程,與使用ajax向伺服器傳送訊息非常類似。想一下,你用ajax向伺服器傳送資料,伺服器是不會直接接受瀏覽器的儲存的資訊的。你需要收集頁面上的資料,把它們放進一個載體裡面(例如JSON),然後傳送資料到遠端伺服器。
同樣的,瀏覽器向GPU傳送資料也需要先建立一個載體;只不過GPU距離CPU很近,不會像遠端伺服器那樣可能幾千裡那麼遠。但是對於遠端伺服器,2秒的延遲是可以接受的;但是對於GPU,幾毫秒的延遲都會造成動畫的卡頓。
瀏覽器向GPU傳送的資料載體是什麼樣?這裡給出一個簡單的製作載體,並把它們傳送到GPU的過程。
- 畫每個複合層的影像
- 準備圖層的資料
- 準備動畫的著色器(如果需要)
- 向GPU傳送資料
所以你可以看到,每次當你新增transform:translateZ(0)
或will-change:transform
給一個元素,你都會做同樣的工作。重繪是非常消耗效能的,在這裡它尤其緩慢。在大多數情況,瀏覽器不能增量重繪。它不得不重繪先前被複合層覆蓋的區域。
隱式合成
還記得剛才a元素和b元素動畫的例子嗎?現在我們將b元素做動畫,a元素靜止不動。
和剛才的例子不同,現在b元素將擁有一個獨立複合層;然後它們將被GPU合成。但是因為a元素要在b元素的上面(因為a元素的z-index比b元素高),那麼瀏覽器會做什麼?瀏覽器會將a元素也單獨做一個複合層!
所以我們現在有三個複合層a元素所在的複合層、b元素所在的複合層、其他內容及背景層。
一個或多個沒有自己複合層的元素要出現在有複合層元素的上方,它就會擁有自己的複合層;這種情況被稱為隱式合成。
瀏覽器將a元素提升為一個複合層有很多種原因,下面列舉了一些:
- 3d或透視變換css屬性,例如translate3d,translateZ等等(js一般通過這種方式,使元素獲得複合層)
- <video><iframe><canvas><webgl>等元素。
- 混合外掛(如flash)。
- 元素自身的 opacity和transform 做 CSS 動畫。
- 擁有css過濾器的元素。
- 使用will-change屬性。
- position:fixed。
- 元素有一個 z-index 較低且包含一個複合層的兄弟元素(換句話說就是該元素在複合層上面渲染)
這看起來css動畫的效能瓶頸是在重繪上,但是真實的問題是在記憶體上:
記憶體佔用
使用GPU動畫需要傳送多張渲染層的影像給GPU,GPU也需要快取它們以便於後續動畫的使用。
一個渲染層,需要多少記憶體佔用?為了便於理解,舉一個簡單的例子;一個寬、高都是300px的純色影像需要多少記憶體?
300 300 4 = 360000位元組,即360kb。這裡乘以4是因為,每個畫素需要四個位元組計算機記憶體來描述。
假設我們做一個輪播圖元件,輪播圖有10張圖片;為了實現圖片間平滑過渡的互動;為每個影像新增了will-change:transform。這將提升影像為複合層,它將多需要19mb的空間。800 600 4 * 10 = 1920000。
僅僅是一個輪播圖元件就需要19m的額外空間!
在chrome的開發者工具中開啟setting——》Experiments——》layers可以看到每個層的記憶體佔用。如圖所示:
GPU動畫的優點和缺點
現在我們可以總結一下GPU動畫的優點和缺點:
- 每秒60幀,動畫平滑、流暢。
- 一個合適的動畫工作在一個單獨的執行緒,它不會被大量的js計算阻塞。
- 3D“變換”是便宜的。
缺點:
- 提升一個元素到複合層需要額外的重繪,有時這是慢的。(即我們得到的是一個全層重繪,而不是一個增量)
- 繪圖層必須傳輸到GPU。取決於層的數量和傳輸可能會非常緩慢。這可能讓一個元素在中低檔裝置上閃爍。
- 每個複合層都需要消耗額外的記憶體,過多的記憶體可能導致瀏覽器的崩潰。
- 如果你不考慮隱式合成,而使用重繪;會導致額外的記憶體佔用,並且瀏覽器崩潰的概率是非常高的。
- 我們會有視覺假象,例如在Safari中的文字渲染,在某些情況下頁面內容將消失或變形。
優化技巧
避免隱式合成
- 保持動畫的物件的z-index儘可能的高。理想的,這些元素應該是body元素的直接子元素。當然,這不是總可能的。所以你可以克隆一個元素,把它放在body元素下僅僅是為了做動畫。
- 將元素上設定will-change CSS屬性,元素上有了這個屬性,瀏覽器會提升這個元素成為一個複合層(不是總是)。這樣動畫就可以平滑的開始和結束。但是不要濫用這個屬性,否則會大大增加記憶體消耗。
動畫中只使用transform和opacity
如上所說,transform和opacity保證了元素屬性的變化不影響文件流、也不受文件流影響;並且不會造成repaint。有些時候你可能想要改變其他的css屬性,作為動畫。例如:你可能想使用background屬性改變背景:
1 2 3 4 5 6 7 8 9 10 |
<div class="bg-change"></div> .bg-change { width: 100px; height: 100px; background: red; transition: opacity 2s; } .bg-change:hover { background: blue; } |
在這個例子中,在動畫的每一步;瀏覽器都會進行一次重繪。我們可以使用一個復層在這個元素上面,並且僅僅變換opacity屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<div class="bg-change"></div> <style> .bg-change { width: 100px; height: 100px; background: red; } .bg-change::before { content: ''; display: block; width: 100%; height: 100%; background: blue; opacity: 0; transition: opacity 20s; } .bg-change:hover::before { opacity: 1; } </style> |
減小複合層的尺寸
看一下兩張圖片,有什麼不同嗎?
這兩張圖片視覺上是一樣的,但是它們的尺寸一個是39kb;另外一個是400b。不同之處在於,第二個純色層是通過scale放大10倍做到的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<div id="a"></div> <div id="b"></div> <style> #a, #b { will-change: transform; } #a { width: 100px; height: 100px; } #b { width: 10px; height: 10px; transform: scale(10); } </style> |
對於圖片,你要怎麼做呢?你可以將圖片的尺寸減少5%——10%,然後使用scale將它們放大;使用者不會看到什麼區別,但是你可以減少大量的儲存空間。
用css動畫而不是js動畫
css動畫有一個重要的特性,它是完全工作在GPU上。因為你宣告瞭一個動畫如何開始和如何結束,瀏覽器會在動畫開始前準備好所有需要的指令;並把它們傳送給GPU。而如果使用js動畫,瀏覽器必須計算每一幀的狀態;為了保證平滑的動畫,我們必須在瀏覽器主執行緒計算新狀態;把它們傳送給GPU至少60次每秒。除了計算和傳送資料比css動畫要慢,主執行緒的負載也會影響動畫; 當主執行緒的計算任務過多時,會造成動畫的延遲、卡頓。
所以儘可能地使用基於css的動畫,不僅僅更快;也不會被大量的js計算所阻塞。
優化技巧總結
- 減少瀏覽器的重排和重繪的發生。
- 不要使用table佈局。
- css動畫中儘量只使用transform和opacity,這不會發生重排和重繪。
- 儘可能地只使用css做動畫。
- 避免瀏覽器的隱式合成。
- 改變複合層的尺寸。
參考
GPU合成主要參考:
https://www.smashingmagazine….
哪些屬性會引起reflow、repaint及composite,你可以在這個網站找到: