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

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

由於GPU合成沒有官方規範,每一個瀏覽器的問題和解決方式也不一樣;因此文章內容僅供參考。html

瀏覽器渲染

提升動畫的優化不得不說起瀏覽器是如何渲染一個頁面。在從服務器中拿到數據後,瀏覽器會先作解析三類東西:web

  • 解析html,xhtml,svg這三類文檔,造成dom樹。ajax

  • 解析css,產生css rule tree。chrome

  • 解析js,js會經過api來操做dom tree和css rule tree。canvas

解析完成以後,瀏覽器引擎會經過dom tree和css rule tree來構建rendering tree:api

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

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將會把多個圖像合成展現在屏幕上。

<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>

clipboard.png

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

<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

<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元素靜止不動。

clipboard.png

和剛纔的例子不一樣,如今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能夠看到每一個層的內存佔用。如圖所示:

clipboard.png

clipboard.png

GPU動畫的優勢和缺點

如今咱們能夠總結一下GPU動畫的優勢和缺點:

  • 每秒60幀,動畫平滑、流暢。

  • 一個合適的動畫工做在一個單獨的線程,它不會被大量的js計算阻塞。

  • 3D「變換」是便宜的。

缺點:

  • 提高一個元素到複合層須要額外的重繪,有時這是慢的。(即咱們獲得的是一個全層重繪,而不是一個增量)

  • 繪圖層必須傳輸到GPU。取決於層的數量和傳輸可能會很是緩慢。這可能讓一個元素在中低檔設備上閃爍。

  • 每一個複合層都須要消耗額外的內存,過多的內存可能致使瀏覽器的崩潰。

  • 若是你不考慮隱式合成,而使用重繪;會致使額外的內存佔用,而且瀏覽器崩潰的機率是很是高的。

  • 咱們會有視覺假象,例如在Safari中的文本渲染,在某些狀況下頁面內容將消失或變形。

優化技巧

避免隱式合成

  • 保持動畫的對象的z-index儘量的高。理想的,這些元素應該是body元素的直接子元素。固然,這不是總可能的。因此你能夠克隆一個元素,把它放在body元素下僅僅是爲了作動畫。

  • 將元素上設置will-change CSS屬性,元素上有了這個屬性,瀏覽器會提高這個元素成爲一個複合層(不是老是)。這樣動畫就能夠平滑的開始和結束。可是不要濫用這個屬性,不然會大大增長內存消耗。

動畫中只使用transform和opacity

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

<div class="bg-change"></div>
.bg-change {
  width: 100px;
  height: 100px;
  background: red;
  transition: opacity 2s;
}
.bg-change:hover {
  background: blue;
}

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

<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>

減少複合層的尺寸

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

clipboard.png

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

<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,你能夠在這個網站找到:

https://csstriggers.com/

相關文章
相關標籤/搜索