一個 Web 頁面的展示,簡單來說可以認為經歷了以下下幾個步驟。
- JavaScript:一般來說,我們會使用 JavaScript 來實現一些視覺變化的效果。比如做一個動畫或者往頁面裡新增一些 DOM 元素等。
- Style:計算樣式,這個過程是根據 CSS 選擇器,對每個 DOM 元素匹配對應的 CSS 樣式。這一步結束之後,就確定了每個 DOM 元素上該應用什麼 CSS 樣式規則。
- Layout:佈局,上一步確定了每個 DOM 元素的樣式規則,這一步就是具體計算每個 DOM 元素最終在螢幕上顯示的大小和位置。web 頁面中元素的佈局是相對的,因此一個元素的佈局發生變化,會聯動地引發其他元素的佈局發生變化。比如,
元素的寬度的變化會影響其子元素的寬度,其子元素寬度的變化也會繼續對其孫子元素產生影響。因此對於瀏覽器來說,佈局過程是經常發生的。
- Paint:繪製,本質上就是填充畫素的過程。包括繪製文字、顏色、影象、邊框和陰影等,也就是一個 DOM 元素所有的可視效果。一般來說,這個繪製過程是在多個層上完成的。
- Composite:渲染層合併,由上一步可知,對頁面中 DOM 元素的繪製是在多個層上進行的。在每個層上完成繪製過程之後,瀏覽器會將所有層按照合理的順序合併成一個圖層,然後顯示在螢幕上。對於有位置重疊的元素的頁面,這個過程尤其重要,因為一旦圖層的合併順序出錯,將會導致元素顯示異常。
當然,本文我們只來關注 Composite 部分。
瀏覽器渲染原理
在討論 Composite 之前,有必要先簡單瞭解下一些瀏覽器(本文只是針對 Chrome 來說)的渲染原理,方便對之後一些概念的理解。更多詳細的內容可以參閱 GPU Accelerated Compositing in Chrome
注:由於 Chrome 對 Blank 引擎某些實現的修改,某些我們之前熟知的類名有了變化,比如 RenderObject 變成了 LayoutObject,RenderLayer 變成了 PaintLayer。感興趣的看以參閱 Slimming Paint。
在瀏覽器中,頁面內容是儲存為由 Node 物件組成的樹狀結構,也就是 DOM 樹。每一個 HTML element 元素都有一個 Node 物件與之對應,DOM 樹的根節點永遠都是 Document Node。這一點相信大家都很熟悉了,但其實,從 DOM 樹到最後的渲染,需要進行一些轉換對映。
從 Nodes 到 LayoutObjects
DOM 樹中得每個 Node 節點都有一個對應的 LayoutObject 。LayoutObject 知道如何在螢幕上 paint Node 的內容。
從 LayoutObjects 到 PaintLayers
一般來說,擁有相同的座標空間的 LayoutObjects,屬於同一個渲染層(PaintLayer)。PaintLayer 最初是用來實現 stacking contest(層疊上下文),以此來保證頁面元素以正確的順序合成(composite),這樣才能正確的展示元素的重疊以及半透明元素等等。因此滿足形成層疊上下文條件的 LayoutObject 一定會為其建立新的渲染層,當然還有其他的一些特殊情況,為一些特殊的 LayoutObjects 建立一個新的渲染層,比如 overflow != visible
的元素。根據建立 PaintLayer 的原因不同,可以將其分為常見的 3 類:
- NormalPaintLayer
- 根元素(HTML)
- 有明確的定位屬性(relative、fixed、sticky、absolute)
- 透明的(opacity 小於 1)
- 有 CSS 濾鏡(fliter)
- 有 CSS mask 屬性
- 有 CSS mix-blend-mode 屬性(不為 normal)
- 有 CSS transform 屬性(不為 none)
- backface-visibility 屬性為 hidden
- 有 CSS reflection 屬性
- 有 CSS column-count 屬性(不為 auto)或者 有 CSS column-width 屬性(不為 auto)
- 當前有對於 opacity、transform、fliter、backdrop-filter 應用動畫
- OverflowClipPaintLayer
- overflow 不為 visible
- NoPaintLayer
- 不需要 paint 的 PaintLayer,比如一個沒有視覺屬性(背景、顏色、陰影等)的空 div。
滿足以上條件的 LayoutObject 會擁有獨立的渲染層,而其他的 LayoutObject 則和其第一個擁有渲染層的父元素共用一個。
從 PaintLayers 到 GraphicsLayers
某些特殊的渲染層會被認為是合成層(Compositing Layers),合成層擁有單獨的 GraphicsLayer,而其他不是合成層的渲染層,則和其第一個擁有 GraphicsLayer 父層公用一個。
每個 GraphicsLayer 都有一個 GraphicsContext,GraphicsContext 會負責輸出該層的點陣圖,點陣圖是儲存在共享記憶體中,作為紋理上傳到 GPU 中,最後由 GPU 將多個點陣圖進行合成,然後 draw 到螢幕上,此時,我們的頁面也就展現到了螢幕上。
渲染層提升為合成層的原因有一下幾種:
注:渲染層提升為合成層有一個先決條件,該渲染層必須是 SelfPaintingLayer(基本可認為是上文介紹的 NormalPaintLayer)。以下所討論的渲染層提升為合成層的情況都是在該渲染層為 SelfPaintingLayer 前提下的。
- 直接原因(direct reason)
- 硬體加速的 iframe 元素(比如 iframe 嵌入的頁面中有合成層)demo
- video 元素
- 覆蓋在 video 元素上的視訊控制欄
- 3D 或者 硬體加速的 2D Canvas 元素
- 硬體加速的外掛,比如 flash 等等
- 在 DPI 較高的螢幕上,fix 定位的元素會自動地被提升到合成層中。但在 DPI 較低的裝置上卻並非如此,因為這個渲染層的提升會使得字型渲染方式由子畫素變為灰階(詳細內容請參考:Text Rendering)
- 有 3D transform
- backface-visibility 為 hidden
- 對 opacity、transform、fliter、backdropfilter 應用了 animation 或者 transition(需要是 active 的 animation 或者 transition,當 animation 或者 transition 效果未開始或結束後,提升合成層也會失效)
- will-change 設定為 opacity、transform、top、left、bottom、right(其中 top、left 等需要設定明確的定位屬性,如 relative 等)demo
- 後代元素原因
- overlap 重疊原因為什麼會因為重疊原因而產生合成層呢?舉個簡單的栗子。藍色的矩形重疊在綠色矩形之上,同時它們的父元素是一個 GraphicsLayer。此時假設綠色矩形為一個 GraphicsLayer,如果 overlap 無法提升合成層的話,那麼藍色矩形不會提升為合成層,也就會和父元素公用一個 GraphicsLayer。此時,渲染順序就會發生錯誤,因此為保證渲染順序,overlap 也成為了合成層產生的原因,也就是如下的正常情形。當然 overlap 的原因也會細分為幾類,接下來我們會詳細看下。
- 重疊或者說部分重疊在一個合成層之上。那如何算是重疊呢,最常見和容易理解的就是元素的 border box(content + padding + border) 和合成層的有重疊,比如:demo,當然 margin area 的重疊是無效的(demo)。其他的還有一些不常見的情況,也算是同合成層重疊的條件,如下:
- 假設重疊在一個合成層之上(assumedOverlap)。這個原因聽上去有點虛,什麼叫假設重疊?其實也比較好理解,比如一個元素的 CSS 動畫效果,動畫執行期間,元素是有可能和其他元素有重疊的。針對於這種情況,於是就有了 assumedOverlap 的合成層產生原因,示例可見:demo。在本 demo 中,動畫元素視覺上並沒有和其兄弟元素重疊,但因為 assumedOverlap 的原因,其兄弟元素依然提升為了合成層。需要注意的是該原因下,有一個很特殊的情況:如果合成層有內聯的 transform 屬性,會導致其兄弟渲染層 assume overlap,從而提升為合成層。比如:demo。
層壓縮
基本上常見的一些合成層的提升原因如上所說,你會發現,由於重疊的原因,可能隨隨便便就會產生出大量合成層來,而每個合成層都要消耗 CPU 和記憶體資源,豈不是嚴重影響頁面效能。這一點瀏覽器也考慮到了,因此就有了層壓縮(Layer Squashing)的處理。如果多個渲染層同一個合成層重疊時,這些渲染層會被壓縮到一個 GraphicsLayer 中,以防止由於重疊原因導致可能出現的“層爆炸”。具體可以看如下 demo。一開始,藍色方塊由於
translateZ
提升為了合成層,其他的方塊元素因為重疊的原因,被壓縮了一起,大小就是包含這 3 個方塊的矩形大小。
當我們 hover 綠色方塊時,會給其設定 translateZ
屬性,導致綠色方塊也被提升為合成層,則剩下的兩個被壓縮到了一起,大小就縮小為包含這 2 個方塊的矩形大小。
當然,瀏覽器的自動的層壓縮也不是萬能的,有很多特定情況下,瀏覽器是無法進行層壓縮的,如下所示,而這些情況也是我們應該儘量避免的。(注:以下情況都是基於重疊原因而言)
- 無法進行會打破渲染順序的壓縮(squashingWouldBreakPaintOrder)示例如下:demo
1234567891011121314151617181920212223242526#ancestor {-webkit-mask-image: -webkit-linear-gradient(rgba(0,0,0,1), rgba(0,0,0,0));}#composited {width: 100%;height: 100%;transform: translateZ(0);}#container {position: relative;width: 400px;height: 60px;border: 1px solid black;}#overlap-child {position: absolute;left: 0;top: 0 ;bottom: 0px;width: 100%;height: 60px;background-color: orange;}
123456<div id="container"><div id="composited">Text behind the orange box.</div><div id="ancestor"><div id="overlap-child"></div></div></div>
- video 元素的渲染層無法被壓縮同時也無法將別的渲染層壓縮到 video 所在的合成層上(squashingVideoIsDisallowed)demo
- iframe、plugin 的渲染層無法被壓縮同時也無法將別的渲染層壓縮到其所在的合成層上(squashingLayoutPartIsDisallowed)demo
- 無法壓縮有 reflection 屬性的渲染層(squashingReflectionDisallowed)demo
- 無法壓縮有 blend mode 屬性的渲染層(squashingBlendingDisallowed)demo
- 當渲染層同合成層有不同的裁剪容器(clipping container)時,該渲染層無法壓縮(squashingClippingContainerMismatch)。示例如下:demo
1234567891011121314151617181920212223.clipping-container {overflow: hidden;height: 10px;background-color: blue;}.composited {transform: translateZ(0);height: 10px;background-color: red;}.target {position:absolute;top: 0px;height:100px;width:100px;background-color: green;color: #fff;}
1234<div class="clipping-container"><div class="composited"></div></div><div class="target">不會被壓縮到 composited div 上</div>
本例中 .target 同 合成層.composited
重疊,但是由於 .composited在一個 overflow: hidden 的容器中,導致 .target 和合成層有不同的裁剪容器,從而
.target無法被壓縮。
- 相對於合成層滾動的渲染層無法被壓縮(scrollsWithRespectToSquashingLayer)示例如下:demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
body { height: 1500px; overflow-x: hidden; } .composited { width: 50px; height: 50px; background-color: red; position: absolute; left: 50px; top: 400px; transform: translateZ(0); } .overlap { width: 200px; height: 200px; background-color: green; position: fixed; left: 0px; top: 0px; } |
1 2 |
<div class="composited"></div> <div class="overlap"></div> |
本例中,紅色的 .composited 提升為了合成層,綠色的
.overlap fix 在頁面頂部,一開始只有
.composited 合成層。
![](https://img.alicdn.com/tps/TB1SHBOMXXXXXbnXFXXXXXXXXXX-690-484.jpg_640x640.jpg)
當滑動頁面,.overlap 重疊到
.composited 上時,
.overlap` 會因重疊原因提升為合成層,同時,因為相對於合成層滾動,因此無法被壓縮。
![](https://img.alicdn.com/tps/TB1IrRGMXXXXXXxaXXXXXXXXXXX-690-484.jpg_640x640.jpg)
- 當渲染層同合成層有不同的具有 opacity 的祖先層(一個設定了 opacity 且小於 1,一個沒有設定 opacity,也算是不同)時,該渲染層無法壓縮(squashingOpacityAncestorMismatch,同 squashingClippingContainerMismatch)demo
- 當渲染層同合成層有不同的具有 transform 的祖先層時,該渲染層無法壓縮(squashingTransformAncestorMismatch,同上) demo
- 當渲染層同合成層有不同的具有 filter 的祖先層時,該渲染層無法壓縮(squashingFilterAncestorMismatch,同上)demo
- 當覆蓋的合成層正在執行動畫時,該渲染層無法壓縮(squashingLayerIsAnimating),當動畫未開始或者執行完畢以後,該渲染層才可以被壓縮 demo
如何檢視合成層
使用 Chrome DevTools 工具來檢視頁面中合成層的情況。
比較簡單的方法是開啟 DevTools,勾選上 Show layer borders
其中,頁面上的合成層會用黃色邊框框出來。
當然,更加詳細的資訊可以通過 Timeline 來檢視。
每一個單獨的幀,看到每個幀的渲染細節:
點選之後,你就會在檢視中看到一個新的選項卡:Layers。
點選這個 Layers 選項卡,你會看到一個新的檢視。在這個檢視中,你可以對這一幀中的所有合成層進行掃描、縮放等操作,同時還能看到每個渲染層被建立的原因。
有了這個檢視,你就能知道頁面中到底有多少個合成層。如果你在對頁面滾動或漸變效果的效能分析中發現 Composite 過程耗費了太多時間,那麼你可以從這個檢視裡看到頁面中有多少個渲染層,它們為何被建立,從而對合成層的數量進行優化。
效能優化
提升為合成層簡單說來有以下幾點好處:
- 合成層的點陣圖,會交由 GPU 合成,比 CPU 處理要快
- 當需要 repaint 時,只需要 repaint 本身,不會影響到其他的層
- 對於 transform 和 opacity 效果,不會觸發 layout 和 paint
利用合成層對於提升頁面效能方面有很大的作用,因此我們也總結了一下幾點優化建議。
提升動畫效果的元素
合成層的好處是不會影響到其他元素的繪製,因此,為了減少動畫元素對其他元素的影響,從而減少 paint,我們需要把動畫效果中的元素提升為合成層。
提升合成層的最好方式是使用 CSS 的 will-change 屬性。從上一節合成層產生原因中,可以知道 will-change 設定為 opacity、transform、top、left、bottom、right 可以將元素提升為合成層。
1 2 3 |
#target { will-change: transform; } |
其相容如下所示:
對於那些目前還不支援 will-change 屬性的瀏覽器,目前常用的是使用一個 3D transform 屬性來強制提升為合成層:
1 2 3 |
#target { transform: translateZ(0); } |
但需要注意的是,不要建立太多的渲染層。因為每建立一個新的渲染層,就意味著新的記憶體分配和更復雜的層的管理。之後我們會詳細討論。
如果你已經把一個元素放到一個新的合成層裡,那麼可以使用 Timeline 來確認這麼做是否真的改進了渲染效能。別盲目提升合成層,一定要分析其實際效能表現。
使用 transform 或者 opacity 來實現動畫效果
文章最開始,我們講到了頁面呈現出來所經歷的渲染流水線,其實從效能方面考慮,最理想的渲染流水線是沒有佈局和繪製環節的,只需要做合成層的合併即可:
為了實現上述效果,就需要只使用那些僅觸發 Composite 的屬性。目前,只有兩個屬性是滿足這個條件的:transforms 和 opacity。更詳細的資訊可以檢視 CSS Triggers。
注意:元素提升為合成層後,transform 和 opacity 才不會觸發 paint,如果不是合成層,則其依然會觸發 paint。具體見如下兩個 demo。
可以看到未提升 target element 為合成層,transform 和 opacity 依然會觸發 paint。
減少繪製區域
對於不需要重新繪製的區域應儘量避免繪製,以減少繪製區域,比如一個 fix 在頁面頂部的固定不變的導航 header,在頁面內容某個區域 repaint 時,整個螢幕包括 fix 的 header 也會被重繪,見 demo,結果如下:
而對於固定不變的區域,我們期望其並不會被重繪,因此可以通過之前的方法,將其提升為獨立的合成層。
減少繪製區域,需要仔細分析頁面,區分繪製區域,減少重繪區域甚至避免重繪。
合理管理合成層
看完上面的文章,你會發現提升合成層會達到更好的效能。這看上去非常誘人,但是問題是,建立一個新的合成層並不是免費的,它得消耗額外的記憶體和管理資源。實際上,在記憶體資源有限的裝置上,合成層帶來的效能改善,可能遠遠趕不上過多合成層開銷給頁面效能帶來的負面影響。同時,由於每個渲染層的紋理都需要上傳到 GPU 處理,因此我們還需要考慮 CPU 和 GPU 之間的頻寬問題、以及有多大記憶體供 GPU 處理這些紋理的問題。
對於合成層佔用記憶體的問題,我們簡單做了幾個 demo 進行了驗證。
demo 1 和 demo 2 中,會建立 2000 個同樣的 div 元素,不同的是 demo 2 中的元素通過 will-change 都提升為了合成層,而兩個 demo 頁面的記憶體消耗卻有很明顯的差別。
防止層爆炸
通過之前的介紹,我們知道同合成層重疊也會使元素提升為合成層,雖然有瀏覽器的層壓縮機制,但是也有很多無法進行壓縮的情況。也就是說除了我們顯式的宣告的合成層,還可能由於重疊原因不經意間產生一些不在預期的合成層,極端一點可能會產生大量的額外合成層,出現層爆炸的現象。我們簡單寫了一個極端點但其實在我們的頁面中比較常見的 demo。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
@-webkit-keyframes slide { from { transform: none; } to { transform: translateX(100px); } } .animating { width: 300px; height: 30px; background-color: orange; color: #fff; -webkit-animation: slide 5s alternate linear infinite; } ul { padding: 5px; border: 1px solid #000; } .box { width: 600px; height: 30px; margin-bottom: 5px; background-color: blue; color: #fff; position: relative; /* 會導致無法壓縮:squashingClippingContainerMismatch */ overflow: hidden; } .inner { position: absolute; top: 2px; left: 2px; font-size: 16px; line-height: 16px; padding: 2px; margin: 0; background-color: green; } |
1 2 3 4 5 6 7 8 9 10 11 |
<!-- 動畫合成層 --> <div class="animating">composited animating</div> <ul> <!-- assume overlap --> <li class="box"> <!-- assume overlap --> <p class="inner">asume overlap, 因為 squashingClippingContainerMismatch 無法壓縮</p> </li> ... </ul> |
demo 中,.animating
的合成層在執行動畫,會導致 .inner
元素因為上文介紹過的 assumedOverlap 的原因,而被提升為合成層,同時,.inner
的父元素 .box
設定了 overflow: hidden
,導致 .inner
的合成層因為 squashingClippingContainerMismatch 的原因,無法壓縮,就出現了層爆炸的問題。
這種情況平時在我們的業務中還是很常見的,比如 slider + list 的結構,一旦滿足了無法進行層壓縮的情況,就很容易出現層爆炸的問題。
解決層爆炸的問題,最佳方案是打破 overlap 的條件,也就是說讓其他元素不要和合成層元素重疊。對於上述的示例,我們可以將 .animation
的 z-index 提高。修改後 demo
1 2 3 4 5 6 7 |
.animating { ... /* 讓其他元素不和合成層重疊 */ position: relative; z-index: 1; } |
此時,就只有 .animating
提升為合成層,如下:
同時,記憶體佔用比起之前也降低了很多。
如果受限於視覺需要等因素,其他元素必須要覆蓋在合成層之上,那應該儘量避免無法層壓縮情況的出現。針對上述示例中,無法層壓縮的情況(squashingClippingContainerMismatch),我們可以將 .box
的 overflow: hidden
去掉,這樣就可以利用瀏覽器的層壓縮了。修改後 demo
此時,由於第一個 .box
因為 squashingLayerIsAnimating 的原因無法壓縮,其他的都被壓縮到了一起。
同時,記憶體佔用比起之前也降低了很多。
最後
之前無線開發時,大多數人都很喜歡使用 translateZ(0)
來進行所謂的硬體加速,以提升效能,但是效能優化並沒有所謂的“銀彈”,translateZ(0)
不是,本文列出的優化建議也不是。拋開了對頁面的具體分析,任何的效能優化都是站不住腳的,盲目的使用一些優化措施,結果可能會適得其反。因此切實的去分析頁面的實際效能表現,不斷的改進測試,才是正確的優化途徑。