Chrome 的加速轉譯功能

圖層模型

Tom Wiltzius
Tom Wiltzius

簡介

對大多數網頁開發人員而言,網頁的基本模型就是 DOM。算繪是一種經常不為人知的程序,會將網頁的這項表示法轉換為螢幕上的圖片。近年來,新式瀏覽器為了充分利用顯示卡,改變了轉譯作業方式:這通常被稱為「硬體加速」。如果是一般網頁 (即非 Canvas2D 或 WebGL),這個術語的真正含意為何?本文將說明 Chrome 中硬體加速轉譯網頁內容的基礎模型。

大胖注意事項

我們在這裡討論的是 WebKit,更具體來說,是 Chromium 的 WebKit 移植版本。本文將說明 Chrome 的實作詳細資料,而非網路平台功能。網路平台和標準不會將這類實作細節編碼,因此無法保證本文的內容能套用至其他瀏覽器,但內部知識仍可用於進階偵錯和效能調整。

另外,請注意,整篇文章都會討論 Chrome 轉譯架構的核心部分,而這個部分的變化速度非常快。本文僅會說明不太可能變更的內容,但無法保證這些內容在六個月後仍會適用。

請務必瞭解,Chrome 已使用兩種不同的轉譯路徑一段時間了:硬體加速路徑和舊版軟體路徑。截至本文撰寫時,所有網頁都會在 Windows、ChromeOS 和 Android 版 Chrome 上採用硬體加速路徑。在 Mac 和 Linux 上,只有需要為部分內容進行合成的網頁才會使用加速路徑 (請參閱下文,進一步瞭解需要合成的內容),但不久之後,所有網頁都會使用加速路徑。

最後,我們將深入探討轉譯引擎的內部運作機制,並研究對效能影響甚大的功能。在嘗試改善網站效能時,瞭解圖層模型可能會有所幫助,但也可能會自掘墳墓:圖層是實用的結構,但建立大量圖層可能會在整個圖形堆疊中引入額外負擔。請注意!

從 DOM 到螢幕

隆重推出圖層

網頁載入及剖析後,瀏覽器會以許多網頁開發人員熟悉的結構呈現:DOM。不過,在轉譯網頁時,瀏覽器會產生一系列中介表示法,但不會直接向開發人員公開。其中最重要的結構是圖層。

在 Chrome 中,其實有幾種不同的圖層:負責 DOM 子樹的 RenderLayers,以及負責 RenderLayers 子樹的 GraphicsLayers。後者對我們來說最有趣,因為 GraphicsLayers 會以紋理形式上傳至 GPU。從現在開始,我會以「圖層」一詞代表 GraphicsLayer。

關於 GPU 術語的簡短說明:紋理是什麼?您可以將其視為從主記憶體 (即 RAM) 移至影片記憶體 (即 GPU 上的 VRAM) 的點陣圖。將圖片載入 GPU 後,您可以將其對應至網格幾何圖形。在電玩遊戲或 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>
旋轉圖層的轉譯邊框螢幕截圖
已旋轉的圖層邊界算繪作業的螢幕截圖

將 3D CSS 屬性放在會旋轉的 <div> 上,即可查看元素取得自身圖層時的樣貌:請注意橘色邊框,這是這個檢視畫面中圖層的輪廓。

圖層建立條件

其他哪些項目會擁有專屬的圖層?Chrome 的這種經驗法則會隨著時間演進,目前會觸發建立層級的事件如下:

  • 3D 或透視變形 CSS 屬性
  • 使用加速影片解碼功能的 <video> 元素
  • <canvas> 使用 3D (WebGL) 環境或加速 2D 環境的元素
  • 複合外掛程式 (例如 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 個像素。這會導致整個元素 (在本例中為整個圖層) 重新排版及重新繪製。

如要查看繪製內容,建議您使用「顯示繪製矩形」工具,該工具位於「顯示」標題下方的「顯示」設定中。開啟後,請注意點按按鈕時,動畫元素和按鈕都會閃爍紅色。

顯示繪製矩形區核取方塊的螢幕截圖
螢幕截圖:顯示繪圖矩形核取方塊

繪圖事件也會顯示在「Dev Tools」時間軸中。眼尖的讀者可能會注意到,這裡有兩個繪圖事件:一個是用於圖層,另一個是用於按鈕本身,當按鈕變更為/從按下狀態時,就會重新繪製。

開發人員工具時間軸重繪圖層的螢幕截圖
開發人員工具時間軸重繪圖層的螢幕截圖

請注意,Chrome 不一定需要重新繪製整個圖層,而是會盡可能只重新繪製 DOM 中已失效的部分。在本例中,我們修改的 DOM 元素是整個圖層的大小。但在許多其他情況下,一個圖層中會有許多 DOM 元素。

接下來的問題很明顯,就是導致無效化並強制重新繪製的原因。要全面回答這個問題並不容易,因為有許多邊緣情況可能會強制無效化。最常見的原因是操控 CSS 樣式或造成重新版面配置,導致 DOM 髒亂。Tony Gentilcore 有篇很棒的部落格文章,說明重新排版的原因,而 Stoyan Stefanov 則有篇文章,更詳細地介紹繪圖 (但只介紹繪圖,而非這項精緻的組合作業)。

如要判斷是否影響您正在處理的內容,最好的方法是使用「開發人員工具」時間軸和「顯示繪圖矩形」工具,查看是否在您不希望重繪時重繪,然後嘗試找出在重新排版/重繪前哪裡污染了 DOM。如果繪圖作業不可避免,但似乎花費過多時間,請參閱 Eberhard Gräther 的文章,瞭解如何在「開發人員工具」中使用持續繪圖模式。

整合:將 DOM 顯示在螢幕上

那麼 Chrome 如何將 DOM 轉換為螢幕圖片呢?概念上,它會:

  1. 取得 DOM 並將其分割成多個層
  2. 將這些圖層各自獨立繪製至軟體點陣圖
  3. 將其上傳至 GPU 做為紋理
  4. 將各個圖層合成最終的螢幕圖片。

這一切都必須在 Chrome 首次產生網頁框架時發生。但之後可以使用一些捷徑來處理未來的幀:

  1. 如果某些 CSS 屬性有所變更,則不必重新繪製任何內容。Chrome 只能將已在 GPU 上以紋理形式存在的現有圖層重新合成,但會使用不同的合成屬性 (例如位於不同位置、具有不同不透明度等)。
  2. 如果某個圖層的部分無效,系統會重新繪製並重新上傳該圖層。如果內容保持不變,但其合成的屬性有所變更 (例如經過轉譯或不透明度有變化),Chrome 可以將其保留在 GPU 上,並重新合成新影格。

如您所見,以圖層為基礎的混合模型對算繪效能有深遠影響。在不需要繪製任何內容的情況下,合成作業的成本相對較低,因此在嘗試對算繪效能進行偵錯時,避免重繪圖層是整體而言不錯的目標。精明的開發人員會查看上述合成觸發事件清單,並發現可以輕鬆強制建立圖層。但請注意,請勿盲目建立這些元素,因為這會占用系統 RAM 和 GPU 的記憶體 (特別是在行動裝置上),而且如果建立太多,邏輯可能會追蹤哪些元素可見,進而造成其他額外負擔。如果圖層太大且彼此重疊,就會導致「過度繪製」現象,導致實際的算繪時間增加。因此,請明智地運用您的知識!

本課程到此告一段落。敬請密切留意,我們將推出更多文章,說明層級模型的實際意涵。

其他資源