圖層模型
簡介
對大多數網頁程式開發人員來說,網頁的基本模型是 DOM。「算繪」通常是指將網頁表示法轉換為螢幕上圖片的模糊過程。近年來,新式瀏覽器改變了轉譯方式,以充分利用顯示卡:這通常模糊地稱為「硬體加速」。談到一般網頁 (即非 Canvas2D 或 WebGL) 時,這個詞彙究竟代表什麼意思?本文將說明 Chrome 中硬體加速轉譯網路內容的基本模型。
重要注意事項
我們在這裡討論的是 WebKit,更具體地說,是 WebKit 的 Chromium 移植版。本文涵蓋 Chrome 的實作細節,而非網路平台功能。網路平台和標準並未將這個層級的實作細節編碼,因此無法保證本文中的任何內容都適用於其他瀏覽器,但瞭解內部機制仍有助於進階偵錯和效能調整。
此外,請注意,本文討論的是 Chrome 轉譯架構的核心部分,變化速度非常快。本文僅涵蓋不太可能變動的內容,但無法保證六個月後仍適用。
請注意,Chrome 有兩種不同的轉譯路徑:硬體加速路徑和舊版軟體路徑。截至本文撰寫時,Windows、ChromeOS 和 Android 版 Google Chrome 上的所有網頁都會採用硬體加速路徑。在 Mac 和 Linux 上,只有部分內容需要合成的網頁才會進入加速路徑 (請參閱下文,進一步瞭解需要合成的內容),但很快所有網頁都會進入加速路徑。
最後,我們將深入探討轉譯引擎,並瞭解對效能有重大影響的功能。嘗試改善網站效能時,瞭解圖層模型會很有幫助,但也很容易弄巧成拙:圖層是實用的建構,但建立大量圖層可能會在整個圖形堆疊中產生負擔。請務必留意!
從 DOM 到螢幕
圖層簡介
網頁載入並剖析後,會以許多網頁程式開發人員熟悉的結構 (DOM) 呈現在瀏覽器中。不過,在算繪網頁時,瀏覽器會有一系列中繼表示法,不會直接向開發人員公開。其中最重要的結構是層。
Chrome 實際上有多種圖層:RenderLayer 負責 DOM 的子樹,GraphicsLayer 負責 RenderLayer 的子樹。後者是我們最感興趣的部分,因為 GraphicsLayer 會以上傳至 GPU 的紋理形式呈現。接下來我會用「圖層」一詞來指稱 GraphicsLayer。
簡單說明 GPU 術語:什麼是紋理?您可以將其視為從主記憶體 (即 RAM) 移至視訊記憶體 (即 GPU 上的 VRAM) 的點陣圖圖片。上傳至 GPU 後,Chrome 就能將材質對應至網格幾何圖形。在電玩遊戲或 CAD 程式中,這項技術可用來為骨架 3D 模型加上「皮膚」。Chrome 會使用材質將網頁內容區塊上傳至 GPU。只要將紋理套用至非常簡單的矩形網格,就能以低成本的方式將紋理對應至不同的位置和轉換。這就是 3D CSS 的運作方式,也很適合快速捲動,但稍後會再詳細說明這兩點。
以下舉幾個例子說明層的概念。
在 Chrome 中研究圖層時,非常實用的工具是開發人員工具設定 (即小齒輪圖示) 中「轉譯」標題下的「顯示合成圖層邊框」標記。這項工具只會醒目顯示圖層在畫面上的位置。我們來開啟這項功能。這些螢幕截圖和範例均取自撰寫本文時最新的 Chrome Canary (Chrome 27)。
圖 1:單層頁面
<!doctype html>
<html>
<body>
<div>I am a strange root.</div>
</body>
</html>
這個頁面只有一個圖層。藍色格線代表圖塊,您可以將其視為圖層的子單位,Chrome 會使用圖塊一次將大型圖層的部分內容上傳至 GPU。這兩者在這裡並不重要。
圖 2:元素位於自己的圖層
<!doctype html>
<html>
<body>
<div style="transform: rotateY(30deg) rotateX(-30deg); width: 200px;">
I am a strange root.
</div>
</body>
</html>
在 <div> 上加入 3D CSS 屬性,使其旋轉,我們就能看到元素取得自己的圖層時會是什麼樣子:請注意橘色邊框,它會在這個檢視畫面中勾勒出圖層。
圖層建立條件
還有哪些項目有自己的圖層?Chrome 的啟發式演算法會隨著時間演進,但目前只要符合下列任一條件,就會觸發圖層建立:
- 3D 或透視轉換 CSS 屬性
<video>個元素使用加速影片解碼- 具有 3D (WebGL) 情境或加速 2D 情境的
<canvas>元素 - 複合外掛程式 (例如 Flash)
- 不透明度採用 CSS 動畫或使用動畫轉換的元素
- 使用加速 CSS 篩選器的元素
- 元素有子系,且該子系有合成層 (換句話說,如果元素有子元素,且該子元素位於自己的層)
- 元素有 Z-index 較低的同層元素,且該同層元素有合成層 (也就是說,元素會顯示在合成層上方)
實際影響:動畫
我們也可以移動圖層,因此圖層非常適合用於動畫。
圖 3:動畫圖層
<!doctype html>
<html>
<head>
<style>
div {
animation-duration: 5s;
animation-name: slide;
animation-iteration-count: infinite;
animation-direction: alternate;
width: 200px;
height: 200px;
margin: 100px;
background-color: gray;
}
@keyframes slide {
from {
transform: rotate(0deg);
}
to {
transform: rotate(120deg);
}
}
</style>
</head>
<body>
<div>I am a strange root.</div>
</body>
</html>
如前所述,圖層非常適合用來移動靜態網路內容。在基本情況下,Chrome 會先將圖層內容繪製成軟體點陣圖,再上傳至 GPU 做為紋理。如果該內容日後不會變更,就不需要重新繪製。這是一件好事:重新繪製需要時間,而這些時間可以花在其他事情上,例如執行 JavaScript,如果繪製時間過長,就會導致動畫出現卡頓或延遲。
舉例來說,請查看開發人員工具時間軸的這個檢視畫面:這個圖層來回旋轉時,沒有任何繪製作業。
無效!重新上漆
但如果圖層內容有變,就必須重新繪製。
圖 4:重新繪製圖層
<!doctype html>
<html>
<head>
<style>
div {
animation-duration: 5s;
animation-name: slide;
animation-iteration-count: infinite;
animation-direction: alternate;
width: 200px;
height: 200px;
margin: 100px;
background-color: gray;
}
@keyframes slide {
from {
transform: rotate(0deg);
}
to {
transform: rotate(120deg);
}
}
</style>
</head>
<body>
<div id="foo">I am a strange root.</div>
<input id="paint" type="button" value="repaint">
<script>
var w = 200;
document.getElementById('paint').onclick = function() {
document.getElementById('foo').style.width = (w++) + 'px';
}
</script>
</body>
</html>
每次點選輸入元素,旋轉元素的寬度就會增加 1 像素。這會導致整個元素 (在本例中為整個圖層) 重新版面配置和重新繪製。
如要查看繪製內容,建議使用開發人員工具中的「顯示繪製矩形」工具,該工具也位於開發人員工具設定的「轉譯」標題下方。開啟後,請注意,點選按鈕時,動畫元素和按鈕都會閃爍紅色。
繪製事件也會顯示在開發人員工具時間軸中。眼尖的讀者可能會注意到,這裡有兩個繪製事件:一個是圖層,另一個是按鈕本身,當按鈕變更為/從按下狀態變更時,就會重新繪製。
請注意,Chrome 不一定需要重新繪製整個圖層,而是會嘗試只重新繪製已失效的 DOM 部分。在本例中,我們修改的 DOM 元素是整個圖層的大小。但在許多其他情況下,圖層中會有多個 DOM 元素。
接下來的問題很顯然是:什麼原因會導致失效,並強制重新繪製?這個問題很難完整回答,因為有許多極端情況會導致失效。最常見的原因是透過操控 CSS 樣式或導致重新版面配置,造成 DOM 髒汙。Tony Gentilcore 撰寫了一篇網誌文章,詳細說明導致重新版面配置的原因,而 Stoyan Stefanov 則撰寫了一篇文章,詳細介紹繪製 (但文章僅介紹繪製,未介紹這個精緻的合成功能)。
如要確認這是否會影響您正在處理的內容,最好的方法是使用開發人員工具時間軸和「顯示繪製矩形」工具,查看是否在不希望重新繪製時重新繪製,然後嘗試找出在重新版面配置/重新繪製之前,您在 DOM 中弄髒的位置。如果繪製作業不可避免,但似乎耗費過多時間,請參閱 Eberhard Gräther 的文章,瞭解開發人員工具中的連續繪製模式。
整合:DOM 到螢幕
那麼,Chrome 如何將 DOM 轉換為螢幕圖片?概念上,它會:
- 擷取 DOM 並分割成多個圖層
- 將每個圖層分別繪製成軟體點陣圖
- 將這些圖片以紋理形式上傳至 GPU
- 將各個圖層組合成最終的螢幕圖片。
上述所有步驟都必須在 Chrome 首次產生網頁頁框時完成。但之後可以採取一些捷徑來處理後續影格:
- 如果某些 CSS 屬性發生變更,則不需要重新繪製任何內容。Chrome 只要重新組合 GPU 上現有的圖層做為紋理,但使用不同的組合屬性 (例如不同的位置、不同的不透明度等)。
- 如果圖層的某個部分失效,系統會重新繪製並重新上傳。如果內容不變,但合成屬性有所變更 (例如經過轉譯或不透明度變更),Chrome 可以將其保留在 GPU 上,並重新合成以建立新影格。
現在應該很清楚,以圖層為基礎的合成模型對轉譯效能有深遠的影響。如果不需要繪製任何內容,合成作業的成本相對較低,因此在偵錯轉譯效能時,避免重新繪製圖層是很好的整體目標。精明的開發人員會查看上方的合成觸發條件清單,並瞭解可以輕鬆強制建立圖層。不過,請勿盲目建立,因為這並非免費:它們會占用系統 RAM 和 GPU 的記憶體 (在行動裝置上尤其有限),而且如果數量過多,可能會在邏輯中產生其他負擔,追蹤哪些是可見的。如果圖層很大,且與先前未重疊的部分重疊,則許多圖層實際上也會增加光柵化的時間,導致有時稱為「過度繪製」的情況。因此,請善用您的知識!
本課程到此告一段落。我們將陸續發布更多文章,探討分層模型的實際應用,敬請密切關注。