Skip to content

Chromium 將 Layer 拆成 Tile,經 Raster Workers 產生 pixel 資源並重組成 Frame,再由 Viz 彙整輸出到螢幕的流程

Chromium Compositing 的運作機制:Tile、Rasterization 與 Viz ​

在上一篇瀏覽器渲染流程中,我們已經知道 Main Thread 會依序處理 Style、Layout 與 Paint,再透過 Commit 把 Paint 資訊與 Layer 交給 Compositor Thread。接下來就要繼續往下看,Compositor 如何把這些資料轉成 pixel 資源、組成 Frame,最後透過 Viz 顯示到螢幕上。

這篇會提到 DrawQuad、CompositorFrame 跟 Viz,這些名稱屬於 Chrome/Chromium 的實作細節,其他瀏覽器不一定會使用相同的名稱。不過整體概念仍然很接近:先把前面 Paint 產生的繪製資訊轉成 pixel 資源,再把這些資源組合成使用者最後看到的畫面。

Compositing ​

為了避免後面的名詞跳來跳去,我們先看一次初次產生畫面時的概念順序:

text
Main Thread 完成 Paint 資訊與 Layer
  → Commit 給 Compositor Thread
  → Layer 拆成 Tile 並安排優先順序
  → Raster Worker Threads 產生 pixel 資源
  → DrawQuad → Render Pass → CompositorFrame
  → Viz 彙整 → Display

實際上的瀏覽器會把許多工作平行處理,也會隨著捲動或內容更新不斷重複這套流程,所以它不一定是一條完全同步的直線。不過按照資料的依賴關係來理解,大致上就是這個順序。

1. Compositor 先把 Layer 拆成 Tile ​

我們在前面提到過,一個 Layer 可能跟整張頁面一樣大。如果每次都直接 Rasterize 整個 Layer,不只要花很多時間,也會占用大量記憶體。因此對一般網頁內容來說,Compositor 會把大型 Layer 拆成許多比較小的 Tile,再依照優先順序安排工作。

瀏覽器也不會毫無差別地把整張網頁都 Rasterize 完才顯示。以 Chromium 來說,Compositor 會根據 Tile 和 Viewport 的距離、捲動方向與可用記憶體安排優先順序,通常會優先處理 Viewport 內及附近即將出現的內容。距離更遠的區域可以晚一點再處理,這樣才不會把時間和記憶體花在使用者目前看不到的內容上。

Viewport 內的 Tile 具有最高 Rasterization 優先度,鄰近 Tile 次之,距離較遠的 Tile 會延後處理

如果使用者捲動得太快,新的 Tile 又還沒準備好,畫面仍可能短暫出現空白、低解析度內容或 Checkerboarding。也就是說,這套機制是在有限資源下盡量讓需要的內容先完成,但不是保證所有離開 Viewport 的內容都已經 Rasterize。

2. Raster Worker Threads 把繪製指令轉成 pixel ​

前面的 Paint 階段留下了「背景要畫在哪裡、文字要怎麼畫」這類繪製指令,而 Rasterization(柵格化)會執行這些指令,把 Tile 的內容轉成 bitmap 或 GPU texture 等 pixel 資源。簡單來說,Paint 是描述要畫什麼,到了 Rasterization 才真的產生 pixel!

這裡要注意,Compositor Thread 比較像是負責拆分、排序與安排工作的人,真正執行柵格化的則是 Raster Worker。TileManager 會根據 Rasterization 的需求建立 TaskGraph,再把 task 排進 worker threads;實際 thread 數量與能否平行處理會依平台,以及使用 software raster 或 GPU raster 而不同,圖中的三條 thread 只是流程示意。完成後的結果會變成後續合成可以引用的資源,可能是 GPU texture,也可能先產生 software bitmap,再上傳給 GPU 使用。

Compositor 將大型 Layer 拆成 Tile 並安排工作,再交由 Raster Worker Threads 產生 pixel 資源

所以把 Tile 理解成一般內容常見的 Rasterization、快取與工作排程單位會比較準確,它不一定是所有類型內容的絕對最小單位。像是影片、WebGL 或本來就已經存在於 GPU 的資源,處理方式就可能不同。

3. Compositor 用 DrawQuad 組成 CompositorFrame ​

當需要的 pixel 資源準備好之後,接下來才會進入真正的合成。Compositor 會決定這些資源要放在哪裡、以什麼順序疊合,以及要套用哪些 transform、clip 或 opacity,並把這些資訊記錄在 DrawQuad 裡。

每個 DrawQuad 可以想成一張矩形的繪製指令,裡面會描述要引用哪個資源、要畫在什麼位置,以及需要套用的合成資訊。所以 Rasterization 產生的資源跟 DrawQuad 不是同一個東西,兩者的關係很像「圖片素材」跟「告訴瀏覽器如何使用這張素材的繪製指令」。而且 DrawQuad 不只可以引用 Tile,也可能用來表達純色、texture、影片或其他 Surface。

這些 DrawQuad 會依照順序放進 Render Pass,最後再連同畫面尺寸、色彩空間等 metadata 組成 CompositorFrame。也就是說,這一段的資料關係會是:

text
pixel resource → DrawQuad → Render Pass → CompositorFrame

DrawQuad 引用 pixel 資源並放進 Render Pass,組成 CompositorFrame 後交給 Viz 彙整並輸出到畫面

4. Viz 彙整 Frame,最後顯示到螢幕 ​

這裡會一次出現 Viz、CompositorFrameSink 跟 SurfaceAggregator 三個新名詞,我們先把它們各自負責的事情拆開來看:

  • Viz:Chromium 中負責管理 Frame、Surface 與最終顯示流程的一整套圖形服務架構。它不是某一個 Frame、Thread 或 GPU,而是接住不同來源畫面並協調後續顯示的系統。
  • CompositorFrameSink:Frame Producer 把 CompositorFrame 提交給 Viz 的介面。Renderer Compositor 組好 Frame 後,會透過這個入口送出去;它本身不負責合成,可以先把它想成「Frame 的收件窗口」。
  • SurfaceAggregator:Viz 內部負責彙整 Surface 中 CompositorFrame 的元件。這裡的 Surface 可以先理解成保存某個 Frame Producer 所提交畫面的容器,例如網頁內容、Browser UI 或不同 Renderer Process 中的 iframe,都可能有自己的 Surface。SurfaceAggregator 會從最上層 Surface 開始,把它引用的其他 Surface 一起收集,並處理它們的 transform、clip、opacity 與 Render Pass,準備出最後要繪製的結果。

把它們串起來之後,流程就會變成 Renderer Compositor 先產生 CompositorFrame,透過 CompositorFrameSink 提交給 Viz,再由 SurfaceAggregator 彙整不同 Surface 的內容並交給 Display Compositor,最後透過 GPU 或 software compositing 呈現在螢幕上:

text
Renderer Compositor
  → CompositorFrame
  → CompositorFrameSink
  → Viz 中的 Surface
  → SurfaceAggregator
  → Display Compositor
  → Display

Renderer Compositor 透過 CompositorFrameSink 將 CompositorFrame 提交到 Viz 的 Surface,再由 SurfaceAggregator 彙整並交給 Display Compositor 輸出到螢幕

因此如果把最後一段只簡化成「CompositorFrame 透過 IPC 送到 Browser Process,再交給 GPU」,就會漏掉現在負責接收、彙整與顯示 Frame 的 Viz。簡單來說,CompositorFrameSink 負責「收件」、SurfaceAggregator 負責「合併」,而 Viz 則負責管理這整套流程。

5. 為什麼捲動畫面時可以直接重組 Frame? ​

走完前面的流程後,瀏覽器手上已經有 Rasterization 產生的 pixel 資源。這時如果只是捲動畫面,或 animation 只影響 transform、opacity、部分 filter 等可由 Compositor 處理的屬性,Compositor Thread 就有機會重用這些資源,調整它們的位置與合成方式,直接組出新的 Frame。

已經 Rasterize 的三個圖層不必重新 Paint,只要調整位置並重新組合,就能產生新的畫面

這樣就不必每次都回到 Main Thread 重新執行 Style、Layout、Paint 跟 Rasterization。不過如果內容本身改變,瀏覽器仍然要重新 Paint,受影響的區域也可能需要重新 Rasterize。

所以 Rasterization 跟 Compositing 是兩個不同的動作:Rasterization 負責「產生 pixel 資源」,Compositing 則負責「使用這些資源組出 Frame」。有些流程圖會把 Rasterization 一起畫在廣義的 Compositing 階段中,因為兩者都在準備最後要送出的畫面;但把它們分開理解,整個流程會清楚很多!

動畫要怎麼善用 transform 與 will-change? ​

在製作位移、縮放或旋轉動畫時,可以優先考慮 transform,而不是直接修改 top、left、width 或 height。因為後面這些屬性通常會讓瀏覽器重新執行 Layout,接著再走 Paint 與 Compositing;如果動畫只改變 transform,瀏覽器就有機會重用已經 Rasterize 的 pixel 資源,直接由 Compositor 組出下一個 Frame。

不過這不代表只要寫了 transform,瀏覽器就一定會建立獨立 Layer,或是保證交給 GPU 處理。Layer 怎麼建立、Rasterization 走 GPU 還是 software,以及最後能不能略過 Layout 與 Paint,仍然會由瀏覽器依照當下的內容與環境決定。

不要把 3D transform 當成「強制開啟 GPU」 ​

以前常見的做法,是透過 translateZ(0) 或 translate3d(0, 0, 0) 讓元素進入不同的合成路徑。這種方式有時確實會讓瀏覽器建立新的 Layer,但它比較像是一種實作技巧,不是穩定的 GPU 開關,也不保證動畫一定會變快。

如果對大量元素套用 3D transform,瀏覽器可能需要管理更多 Layer 與 GPU texture,同時增加記憶體使用量。結果不只沒有比較順,反而可能讓低記憶體裝置更容易卡頓。所以真的遇到動畫效能問題時,還是要回到 DevTools 的 Performance 或 Layers 面板,確認瓶頸是不是發生在 Layout、Paint 或 Compositing。

will-change 是提前給瀏覽器的提示 ​

will-change 則是在告訴瀏覽器:「這個屬性接下來可能會改變。」例如設定 will-change: transform,瀏覽器就有機會提前準備後續動畫需要的資源。不過它同樣只是提示,不代表一定會建立獨立 Layer,也不是強制使用 GPU 的命令。

因此不建議直接替所有 HTML 元素加上 will-change。瀏覽器為了準備這些變化,可能會提前配置額外資源;如果提示的元素太多,反而會增加記憶體與渲染成本。應該先透過實際量測確認問題,真的需要時,再只對少數即將發生動畫的元素使用,並且在動畫結束後移除:

javascript
const card = document.querySelector('.card')

function prepareAnimation() {
  card.style.willChange = 'transform'
}

function cleanupAnimation() {
  card.style.willChange = 'auto'
}

card.addEventListener('pointerenter', prepareAnimation)
card.addEventListener('transitionend', cleanupAnimation)

這裡在使用者準備互動時先加上提示,等 transition 結束後再還原成 auto。實際要提早多久加入,還是要看動畫的觸發方式;重點不是每次都套用同一段程式碼,而是讓 will-change 只存在於真正需要的時間內。

以下三個 demo 分別示範沒有使用、在適當時機使用,以及把所有 DOM node 都加上 will-change 的版本。實際差異會依裝置與瀏覽器而不同,但第三個版本刻意全部加上 will-change,很可能會增加資源消耗,甚至讓整個頁面卡到不行!所以你看完所有版本後,還會想把它加好加滿嗎?

參考資料 ​

按 Enter 或空白鍵放大圖片,按 Escape 關閉放大檢視。