一篇文章說清瀏覽器解析和CSS(GPU)動畫優化

發表於2017-01-10

相信不少人在做移動端動畫的時候遇到了卡頓的問題,這篇文章嘗試從瀏覽器渲染的角度;一點一點告訴你動畫優化的原理及其技巧,作為你工作中優化動畫的參考。文末有優化技巧的總結。

因為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。例如:

以上第三行程式碼,需要瀏覽器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將會把多個影象合成展示在螢幕上。

1861898982-586c5a0d3a1c9_articlex

我們將A元素使用left屬性,做一個移動動畫:

在這個例子中,對於動畫的每一幀;瀏覽器會計算元素的幾何形狀,渲染新狀態的影象;並把它們傳送給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

瀏覽器在動畫執行之前就知道動畫如何開始和結束,因為瀏覽器沒有看到需要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元素靜止不動。

4036726279-586cb0712d519_articlex

和剛才的例子不同,現在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可以看到每個層的記憶體佔用。如圖所示:

399595312-586cbd8476c4b_articlex

601380865-586cbd9b9106a_articlex

GPU動畫的優點和缺點

現在我們可以總結一下GPU動畫的優點和缺點:

  • 每秒60幀,動畫平滑、流暢。
  • 一個合適的動畫工作在一個單獨的執行緒,它不會被大量的js計算阻塞。
  • 3D“變換”是便宜的。

缺點:

  • 提升一個元素到複合層需要額外的重繪,有時這是慢的。(即我們得到的是一個全層重繪,而不是一個增量)
  • 繪圖層必須傳輸到GPU。取決於層的數量和傳輸可能會非常緩慢。這可能讓一個元素在中低檔裝置上閃爍。
  • 每個複合層都需要消耗額外的記憶體,過多的記憶體可能導致瀏覽器的崩潰。
  • 如果你不考慮隱式合成,而使用重繪;會導致額外的記憶體佔用,並且瀏覽器崩潰的概率是非常高的。
  • 我們會有視覺假象,例如在Safari中的文字渲染,在某些情況下頁面內容將消失或變形。

優化技巧

避免隱式合成

  • 保持動畫的物件的z-index儘可能的高。理想的,這些元素應該是body元素的直接子元素。當然,這不是總可能的。所以你可以克隆一個元素,把它放在body元素下僅僅是為了做動畫。
  • 將元素上設定will-change CSS屬性,元素上有了這個屬性,瀏覽器會提升這個元素成為一個複合層(不是總是)。這樣動畫就可以平滑的開始和結束。但是不要濫用這個屬性,否則會大大增加記憶體消耗。

動畫中只使用transform和opacity

如上所說,transform和opacity保證了元素屬性的變化不影響文件流、也不受文件流影響;並且不會造成repaint。有些時候你可能想要改變其他的css屬性,作為動畫。例如:你可能想使用background屬性改變背景:

在這個例子中,在動畫的每一步;瀏覽器都會進行一次重繪。我們可以使用一個復層在這個元素上面,並且僅僅變換opacity屬性:

減小複合層的尺寸

看一下兩張圖片,有什麼不同嗎?

2942097818-586cc556bf0c1_articlex

這兩張圖片視覺上是一樣的,但是它們的尺寸一個是39kb;另外一個是400b。不同之處在於,第二個純色層是通過scale放大10倍做到的。

對於圖片,你要怎麼做呢?你可以將圖片的尺寸減少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,你可以在這個網站找到:

https://csstriggers.com/

相關文章