詳談層合成(composite)

前不久寫了一篇關於如何使用 Chrome DevTools 優化高德地圖動畫的文章,其中提到了 composite,可是並無細談。思考許久,仍是以爲有必要再總結一下。
css

1、什麼是 composite ?

通俗來講:在 DOM 樹中每一個節點都會對應一個 LayoutObject,當他們的 LayoutObject 處於相同的座標空間時,就會造成一個 RenderLayers ,也就是渲染層。
RenderLayers 來保證頁面元素以正確的順序合成,這時候就會出現層合成(composite),從而正確處理透明元素和重疊元素的顯示。node

一旦加載並解析頁面,它就在瀏覽器中做爲許多Web開發人員熟悉的結構來表示:DOM。然而,當呈現一個頁面時,瀏覽器有一系列不直接暴露給開發者的中間表示。這些結構中最重要的是層。git

到這裏,層的概念是有了。composite 翻譯過來就是咱們常說的合成,那麼他是怎麼工做的?github

2、RenderLayers 與 GraphicsLayers

先貼出一張流程圖,稍後咱們細說:
layer trees
在 Chrome 中其實有幾種不一樣的層類型:web

  • RenderLayers 渲染層,這是負責對應 DOM 子樹
  • GraphicsLayers 圖形層,這是負責對應 RenderLayers 子樹。

提到 RenderLayers 不得不說 RenderObjects :
RenderObjects 保持了樹結構,一個 RenderObjects 知道如何繪製一個 node 的內容, 他經過向一個繪圖上下文(GraphicsContext)發出必要的繪製調用來繪製 nodes。chrome

每一個 GraphicsLayer 都有一個 GraphicsContext,GraphicsContext 會負責輸出該層的位圖,位圖是存儲在共享內存中,做爲紋理上傳到 GPU 中,最後由 GPU 將多個位圖進行合成,而後 draw 到屏幕上,此時,咱們的頁面也就展示到了屏幕上。
編程

GraphicsContext 繪圖上下文的責任就是向屏幕進行像素繪製(這個過程是先把像素級的數據寫入位圖中,而後再顯示到顯示器),在chrome裏,繪圖上下文是包裹了的 Skia(chrome 本身的 2d 圖形繪製庫)canvas


某些特殊的渲染層會被認爲是合成層(Compositing Layers),合成層擁有單獨的 GraphicsLayer,而其餘不是合成層的渲染層,則和其第一個擁有 GraphicsLayer 父層公用一個。

3、composite 隱式合成

對於隱式合成,CSS GPU Animation 中是這麼描述的:
瀏覽器

This is called implicit compositing: One or more non-composited elements that should appear above a composited one in the stacking order are promoted to composite layers — i.e. painted to separate images that are then sent to the GPU.緩存


大概意思就是:一個或多個非合成元素應出如今堆疊順序上的合成元素之上,被提高到合成層,即被繪製成分離的圖像,而後將圖像交給 GPU 處理。

咱們先來看下面的圖示:

假設一種場景,咱們須要 A 顯示在 B 之上,而後爲 B 添加移動的動畫,這裏就會出現一個邏輯問題:B 由於有動畫,被提高到了合成層,最終在 GPU 上合成了屏幕圖像,而 A 須要顯示在 B 之上,咱們並無作任何處理。因此爲了使 A 和 B 正常顯示,咱們須要設置 z-index ,這時瀏覽器將強制提高 A 爲複合層,隨後進行 repaint。

這時候,composite 隱式合成就出現了。固然,還有更多的場景,咱們繼續。

4、影響 composite 因素

咱們知道,在某些特定條件下,瀏覽器會主動將渲染層提至合成層,那麼影響 composite 的因素有哪些?

  1. 3D transforms: translate3d, translateZ 等;
  2. video, canvas, iframe 等元素;
  3. 經過 Element.animate() 實現的 opacity 動畫轉換;
  4. 經過 СSS 動畫實現的 opacity 動畫轉換;
  5. position: fixed;
  6. will-change;
  7. filter;
  8. 有合成層後代同時自己 overflow 不爲 visible(若是自己是由於明確的定位因素產生的 SelfPaintingLayer,則須要 z-index 不爲 auto)
    等等…

這裏只舉出部分例子,無線性能優化:Composite這篇文章描述的很詳細,這裏就不贅述了。

5、層壓縮與層爆炸

1.層壓縮:
相似咱們舉出的層隱式合成的例子,可能簡單的重疊就會產生大量的合成層,這樣會佔用不少無辜的 CPU 和 內存資源,嚴重影響了頁面的性能。這一點瀏覽器也考慮到了,所以就有了層壓縮(Layer Squashing)的處理。

瀏覽器的自動的層壓縮也不是萬能的,有不少特定狀況下,瀏覽器是沒法進行層壓縮的。


以下所示,而這些狀況也是咱們應該儘可能避免的:

  • 沒法進行會打破渲染順序的壓縮
  • video 元素的渲染層沒法被壓縮同時也沒法將別的渲染層壓縮到 video 所在的合成層上
  • iframe、plugin 的渲染層沒法被壓縮同時也沒法將別的渲染層壓縮到其所在的合成層上
  • 沒法壓縮有 reflection 屬性的渲染層(squashingReflectionDisallowed)
  • 沒法壓縮有 blend mode 屬性的渲染層(squashingBlendingDisallowed)
  • 當渲染層同合成層有不一樣的裁剪容器(clipping container)時,該渲染層沒法壓縮(squashingClippingContainerMismatch)
  • 相對於合成層滾動的渲染層沒法被壓縮(scrollsWithRespectToSquashingLayer)
  • 當渲染層同合成層有不一樣的具備 opacity 的祖先層(一個設置了 opacity 且小於 1,一個沒有設置 opacity,也算是不一樣)時,該渲染層沒法壓縮(squashingOpacityAncestorMismatch,同 squashingClippingContainerMismatch)
  • 當渲染層同合成層有不一樣的具備 transform 的祖先層時,該渲染層沒法壓縮(squashingTransformAncestorMismatch,同上)
  • 當渲染層同合成層有不一樣的具備 filter 的祖先層時,該渲染層沒法壓縮(squashingFilterAncestorMismatch,同上)
  • 當覆蓋的合成層正在運行動畫時,該渲染層沒法壓縮(squashingLayerIsAnimating),當動畫未開始或者運行完畢之後,該渲染層才能夠被壓縮

此處摘錄自無線性能優化:Composite,demo 請查看原文。

若是多個渲染層同一個合成層重疊時,這些渲染層會被壓縮到一個 GraphicsLayer 中,以防止因爲重疊緣由致使可能出現的「層爆炸」。

2.層爆炸:
經過以前的介紹,咱們知道同合成層重疊也會使元素提高爲合成層,雖然有瀏覽器的層壓縮機制,可是也有不少沒法進行壓縮的狀況。也就是說除了咱們顯式的聲明的合成層,還可能因爲重疊緣由不經意間產生一些不在預期的合成層,極端一點可能會產生大量的額外合成層,出現層爆炸的現象。

解決層爆炸的問題,最佳方案是打破 overlap 的條件,也就是說讓其餘元素不要和合成層元素重疊,譬如巧妙的使用 z-index 屬性。

6、內存消耗

上面提到了層合成的過程會產生內存消耗,那麼咱們如何來評估層消耗的內存,下面舉例來講明:

<!-- jartto test -->
<div id="a"></div>
<div id="b"></div>複製代碼

#a, #b {
 will-change: transform;
}
#a {
 width: 100px;
 height: 100px;
 background: rgb(255, 0, 0);
}
#b {
 width: 10px;
 height: 10px;
 background: rgb(255, 0, 0);
 transform: scale(10);
}複製代碼

如上,咱們建立了兩個容器 #a 和 #b,#a 的物理尺寸是 100×100px(100×100×3 = 30000 字節),而 #b 只有10×10px(10×10×3 = 300 字節)但放大了 10 倍。#b 因爲存在 will-change 屬性,transform 動畫將經過 GPU 來渲染圖層。

咱們經過圖像的高度乘以圖像的寬度來得到圖像中像素的數量。而後,咱們將其乘以3,由於每一個像素都用三個字節(RGB)描述。那麼不難理解,若是圖像包含透明區域,咱們要乘以4,由於須要額外的字節來描述透明度:(RGBA):100×100×4 = 40000 字節。

從上面的例子中,咱們得出了一個很是有意義的結論,從而幫助咱們去作一些簡單的優化。例如:若是你想要爲一張大圖添加動畫,你能夠先下載縮小版(原版 10% )而後放大顯示。這對用戶是無感知的,可是咱們卻精簡了頁面加載,從而提高了用戶體驗。

7、Reflowing and Repainting

很好,咱們能夠經過上述來評估內存消耗了,這裏引出了兩個術語 Reflow 和 Repaint ,簡單溫習一下:

  • Reflow(迴流):瀏覽器要花時間去渲染,當它發現了某個部分發生了變化影響了佈局,那就須要倒回去從新渲染。
  • Repaint(重繪):若是隻是改變了某個元素的背景顏色,文字顏色等,不影響元素周圍或內部佈局的屬性,將只會引發瀏覽器的repaint,重畫某一部分。

Reflow要比Repaint更花費時間,也就更影響性能。因此在寫代碼的時候,要儘可能避免過多的Reflow。

reflow 的緣由:

  • 頁面初始化的時候;
  • 操做DOM時;
  • 某些元素的尺寸變了;
  • 若是 CSS 的屬性發生變化了。

減小 reflow / repaint

  • 不要一條一條地修改 DOM 的樣式。與其這樣,還不如預先定義好 css 的 class,而後修改 DOM 的 className。
  • 不要把 DOM 結點的屬性值放在一個循環裏當成循環裏的變量。
  • 爲動畫的 HTML 元件使用 fixed 或 absoult 的 position,那麼修改他們的 CSS 是不會 reflow 的。
  • 千萬不要使用 table 佈局。由於可能很小的一個小改動會形成整個 table 的從新佈局。

8、硬件加速

既然整個渲染過程如此耗時,那麼大多數人喜歡使用 translateZ(0) 與 will-change 開啓硬件加速的行爲就很容易理解了。

轉了一圈,又回到了本文的重點:合成層。提高合成層的最好方式是使用 CSS 的 will-change 屬性。而 will-change 設置爲 opacity、transform、top、left、bottom、right 能夠將元素提高爲合成層。

先來看看 will-change 的瀏覽器支持狀況,點擊查看
will-change
整體支持狀況還不錯,因此咱們能夠像下面這樣使用:

#jartto {
  will-change: transform;
}複製代碼

固然,對於個別不支持的瀏覽器,咱們使用 translateZ(0) 來解決,點擊查看
translate

#jartto {
  transform: translateZ(0);
}複製代碼

We already know that animation of transform and opacity via CSS transitions or animations automatically creates a compositing layer and works on the GPU.

那麼問題來了,硬件加速依賴 GPU ,而 GPU 爲何會比 CPU 快,咱們接着來看。

9、CPU(中央處理器)和 GPU(圖形處理器)

文中反覆提到了 CPU 和 GPU ,相信不少童鞋可能會產生這樣的疑惑:爲何要開啓硬件加速,以及 GPU 優點到底在哪裏?

咱們先簡單的瞭解一下 GPU 的工做原理,GPU 處理數據的過程大概是這樣的:

  • 將每一個複合層繪製成一個單獨的圖像;
  • 準備層數據(尺寸、偏移量、透明度等);
  • 準備動畫着色器(若是適用);
  • 將數據發送到GPU;

pu-accelerated-compositing-in-chrome 這篇文章能夠看出,硬件合成的好處有三種:

  1. GPU 上合成圖層能夠在涉及大量像素的繪圖和合成操做中實現比 CPU(不管是在速度和功耗方面)還要好的效率。硬件專爲這些類型的工做負載而設計。
  2. GPU 上的內容不須要昂貴的回讀(例如加速視頻 Canvas2D 或 WebGL )。
  3. CPU 和 GPU 之間的並行性,能夠同時運行以建立高效的圖形管道。

組成緩存元素的圖像會更快,而這正是 GPU 的強勢之處:它可以很快地用亞像素精度合成圖像,這給動畫增長了顯著的平滑度。

咱們能夠這麼理解,GPU 是一個單獨的計算機:每個現代設備的一個重要部分其實是一個獨立的單元,有本身的處理器和本身的內存和數據處理模型。與其餘應用程序或遊戲同樣,瀏覽器必須像外部設備那樣與 GPU 對話。

更多 GPU 的介紹,請看這裏傳送門,這裏就不扯遠了。

10、補充:CPU 與 GPU 各自的職責

這裏我再補充一點:

咱們能夠說 CPU 所作的工做都在軟件層面,而 GPU 在硬件層面,咱們能夠用軟件(使用 CPU )作任何事情,可是對於圖像處理,一般用硬件會更快,由於GPU使用圖像對高度並行浮點運算作了優化。

這也是我以前一直困擾的地方,咱們一味的強調硬件加速,而忽略了 CPU 自己的做用。大體過程可能以下:

  1. CPU 計算好顯示內容提交到 GPU
  2. GPU 渲染完成後將渲染結果放入幀緩衝區
  3. 視頻控制器會按照 VSync 信號逐行讀取幀緩衝區的數據,通過可能的數模轉換傳遞給顯示器顯示

咱們看到了 GPU 確實很強勢,可是咱們最好不要把全部東西一古腦兒拋給 GPU ,問題在於 GPU 並無無限制處理性能,並且一旦資源用完的話,性能就會開始降低了(即便 CPU 並無徹底佔用)。事實上他們有本身的職責,各司其職,各盡其才,才能發揮出更大的做用。

11、工具使用

咱們不打無準備的仗,合理的利用工具,才能大大提升編程效率。這裏自薦一篇文章優化高德地圖動畫,內容包括:

  • 頁面性能檢測
  • Chrome 動畫調試器(Animations)
  • 頁面渲染狀況(Rendering)
  • 圖層(Layers)
  • GPU 與 複合層

原諒我標題黨了,起初是爲了找尋優化高德地圖動畫的方案,結果寫成了實際的例子,一步步介紹了 Chrome DevTools 。文章已經發表,因此就沒在去更改題目,若是能夠的話,我但願的題目是:動畫優化之如何使用 Chrome DevTools

12、優化建議

提高爲合成層簡單說來有如下幾點好處:

  • 合成層的位圖,會交由 GPU 合成,比 CPU 處理要快
  • 當須要 repaint 時,只須要 repaint 自己,不會影響到其餘的層
  • 元素提高爲合成層後,transform 和 opacity 纔不會觸發 paint,若是不是合成層,則其依然會觸發 paint。

若是你已經把一個元素放到一個新的合成層裏,那麼可使用 Timeline 來確認這麼作是否真的改進了渲染性能。別盲目提高合成層,必定要分析其實際性能表現。

實際上,在內存資源有限的設備上,合成層帶來的性能改善,可能遠遠趕不上過多合成層開銷給頁面性能帶來的負面影響。同時,因爲每一個渲染層的紋理都須要上傳到 GPU 處理,所以咱們還須要考慮 CPU 和 GPU 之間的帶寬問題、以及有多大內存供 GPU 處理這些紋理的問題。

因此,你就會明白,這裏咱們使用方式二而不使用方式一的緣由了:

/*jartto:方式一*/
@keyframes move {
 from { left: 30px; }
 to { left: 100px; }
}複製代碼

/*jartto:方式二*/
@keyframes move {
 from { transform: translateX(0); }
 to { transform: translateX(70px); }
}複製代碼

十3、總結

優化實際上是一個過程,咱們須要一個點一個點的處理、突破。沒有什麼是一蹴而就的,更沒有所謂的銀彈。就像高中物理書中所說的偏差:「偏差是不可避免的,只能減小」。

優化也同樣,咱們只能盡力去作,而不能強求。不斷嘗試,方是永恆。

參考:
無線性能優化:Composite
CSS GPU Animation
web優化之composite
瀏覽器渲染
gpu-accelerated-compositing-in-chrome
視圖渲染、CPU和GPU卡頓緣由及其優化方案

相關文章
相關標籤/搜索