關鍵轉譯路徑 Critical Rendering Path

假設畫面有任何變動,例如滾動捲軸而產生動畫效果,裝置將會更新螢幕。現今裝置更新畫面的頻率是每秒 60 幀(60Hz 或稱 60fps),意即每幀運行的時間最多是 16.67ms。但瀏覽器不僅要渲染畫面,還有很多事情要忙,因此每幀運行的時間只能約 10 ~ 12ms。若瀏覽器拖太久才更新畫面,就會產生顫動(Juddering)。想提高更新畫面的頻率、避免顫動,就要了解瀏覽器如何渲染畫面。

Browser Rendering Pipeline

瀏覽器渲染的過程可看成是一連串的步驟,也就是像素管道(Browser Rendering Pipeline)。

Browser Rendering Pipeline

圖片來源:Rendering Performance

說明

在渲染的過程中,不需每個環節都走過,可選用經過最少階段的指令,像是…

使用 CSS Triggers 來查詢指令會觸發和走過哪些階段。

範例

使用 flexbox 排列一連串的盒子,盒子從小變大。請問會走過哪些環節?Style?Layout?Paint?Composite?

範例-flexbox

圖片來源:The Critical Rendering Path

答案是 Layout、Paint、Composite。

由於指令不需重新計算,因此不經過 Style。當螢幕改變大小時,就需要重新計算元素大小與所佔空間(Layout),因此也會需要繪製(Paint)與合成(Composite)。注意,當觸發螢幕改變大小時,若是使用 Media Query 或 JavasSript 改變樣式規則,就會需要經過 Style。

以下再更詳細說明 Browser Rendering Pipeline 的每個步驟。

樣式計算(Style Calculations)

瀏覽器渲染畫面的過程是這樣的…使用者想要瀏覽某個頁面,於是瀏覽器對伺服器發出請求,接著伺服器以 HTML 的方式回應,瀏覽器開始分析 HTML 以建立 DOM Tree。在 HTML 中找到 CSS,可能是 inline style 或外部連結檔案,結合 DOM 與 CSS 來做 Recalculation Style,也就是匹配元素與樣式規則,匹配完畢就產生 Render Tree,之後會利用 Render Tree 計算每個可視元素的版面配置。

Render Tree

Render Tree 類似 DOM Tree,但 Render Tree 只存在可見的元素,不可見的都會被移除。

以下不可見的節點會從 Render Tree 移除

但以下節點依然可見,所以仍會存在於 Render Tree

section h1:after {
  content: 'Hello World';
}

如何減少解析樣式規則成本?

減少解析樣式規則成本的方法有二:(1) 減少需要查看匹配元素的範圍 (2) 簡化 CSS Selector 的複雜度。

方法一:減少需要查看匹配元素的範圍

減少受影響的元素數量,這樣 Render Tree 的更新會比較少。

這個規則會檢視當前頁面所有的 <a> 標籤。

a {
  /* styles */
}

這個規則僅會檢查使用 .link 這個 class 的標籤。

.link {
  /* styles */
}

注意,盡量減少樣式規則層數,至多三層。

方法二:簡化 CSS Selector 的複雜度

降低樣式規則的複雜度,愈簡單的規則,愈容易在 Render Tree 上查找,所花的時間就愈少。

這個規則是簡單易懂的。

.title {
  /* styles */
}

這個規則是較難理解的,但隨著專案變大,一定會需要撰寫較複雜的規則。

.box:nth-last-child(-n + 1) .title {
  /* styles */
}

那麼就改進為這個…

.final-box-title {
  /* styles */
}

使用類似 BEM 的 CSS class 命名規則,可簡化 CSS Selector 的複雜度。因為 BEM 能建立以 class 為主的單一層規則,並能符合在大專案中需要的複雜階層。

例如

.list {
  /* styles */
}
.list__list-item {
  /* styles */
}
.list__list-item--last-child {
  /* styles */
}

點此看比較各種 CSS 的模組化方法-OOCSS、SMACSS、BEM、CSS Modules、CSS in JS。

效能評估

在 Chrome DevTools Timeline 上檢視 Recalculation Style 階段,可查看花費時間、影響元素等更多細節,若超過 16ms 就可能會發生顫動。

Recalculation Style

版面配置(Recaculate Layout / Reflow)

在樣式計算的階段,瀏覽器知道什麼樣的 CSS 規則要套用到哪一個 DOM Element 後,接著就可以開始計算每個可視元素的版面配置,這個分配元素位置的過程稱為 Recaculate Layout 或 Reflow,在 Chrome DevTools 上稱為 Layout。

如何提升版面配置的效能?

影響元素大小或位置的指令會導致瀏覽器做 Reflow,例如:width、height、margin、position 等。Layout 的作用範圍通常是整個 Document,若希望減少 Reflow 的成本,可將 Reflow 的範圍縮小在特定 DOM Element 內,例如使用能限制範圍的元素。

能限制範圍的元素有以下特性

或從工具查看可改善的地方

最後可從 Layout Scope、影響的 Node 數和經歷的 Duration 等方面來檢視改善效果。

除了縮小 Reflow 範圍外,也可從以下方式來減少成本

避免強制同步版面配置

圖片來源:Making a Silky Smooth Web

範例 1

如何使用 Chrome DevTools Timeline 檢視 Layout Scope?點此看 Demo,使用者可對整個頁面做點擊,接著依序更改特定元素的寬度

因此,更改哪一個區塊的寬,會讓瀏覽器花最多的工來 Reflow 畫面?

答案是 C。

檢視 Chrome DevTools 的 tab「Performance」,錄製整個過程-點擊畫面三次,發現都會更新整個網頁(document),因為更新範圍,即 Layout Scope 相同,推知瀏覽器 Reflow 所花的工是相同的。

第一次點擊會更改 <body> 的寬。

Reflow 範例-第一次點擊

點擊「Layout」這個紫色的區塊,瀏覽器會將 Layout Scope 用陰影標記出來。

第三次點擊會更改 <div id="d2"> 的寬。

Reflow 範例-第三次點擊

範例 2

如何使用 Boundarizr 檢視 Layout Scope? 在要檢視的頁面載入這個 library 後,若頁面的 DOM Element 可當作 Layout Boundary,就會用陰影的方式標記出來,例如以下這個 <input>

<input type="text" placeholder="Hello World" />

Layout Boundary

若要取消標記,點擊上方按鈕「Hide Boundaries」即可。

由於這個工具會用到 getComputedStyle,Safari 仍可使用,而 Chrome 卻不能用,因為從版本 63 後 getComputedStyle 已被廢棄,因此提了 issue 希望作者可以修復,待之後玩玩看摟。

繪製(Paint)

將樣式繪製成像素(Vector of Raster / Rasterizer)。

變更 transform 或 opacity 之外的任何屬性,一定會觸發繪製。例如:(1) 變更幾何形狀 width 或 height,觸發 Layout 後必定會經過 Paint;(2) 變更文字或背景必定會觸發 Paint。影響 Paint 的指令有 box-shadow、border-radius、color、background-color、text-shadow 等。

如何提升繪製的效能?

方法一:升階

繪製耗時的其中一個原因是瀏覽器會聯合需要繪製的區域,而導致整個螢幕重繪,因此若能建立一個新的層,就不會影響其他元素,而能減少繪製區域。在這裡使用 will-changetranslateZ(0) 將移動或淡化的元素升階,這是告知瀏覽器提前準備資源、建立合成器圖層以因應改變,但很耗費記憶體,可能招致反效果,要謹慎使用。

.moving-element {
  will-change: transform;
}

.moving-element {
  transform: translateZ(0);
}

方法二:簡化繪製複雜度

任何涉及模糊 (例如:陰影) 的屬性會花更長時間才能完成繪製,若非必要,可使用替代方案。

例如,在範例 1 中,第一次點擊時,觸發繪製耗時 58μs,其中 <body> 這個紅色區塊的指令如下。

body {
  background: red;
}

範例 1<body> 紅色區塊的指令稍做修改,加上陰影,繪製耗時增加為 70μs。

body {
  background: red;
  box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.5);
}

效能評估

由於繪製是 pipeline 中最耗時的步驟,應盡量避免或降低繪製,可使用 Chrome DevTools 繪製分析工具以評估繪製複雜性和成本。

Paint Flashing

打開 Paint Flashing 功能,位於 Chrome DevTools > More tools > Rendering,勾選「Paint Flashing」,當發生繪製的時候,發生之處會閃綠光。可依此判斷此處是否需要繪製來做調整。

Paint Flashing

Timeline

執行 Performance 的錄製,記錄在繪製階段的資訊,例如繪製所需要的時間。

Paint Timeline

Draw Bitmap

這裡包含 Draw Bitmap 的步驟-圖檔通常會存成 JPEG、PNG 或 GIF 等,瀏覽器將這些檔案解碼並存到記憶體終以供使用,若是響應式網頁可能還需做大小調整,這個過程在 Chrome DevTools 上被稱為「Image Decode + Resize」。

合成(Composite Layers)

瀏覽器依序輸出層疊樣式。

如何提升合成的效能?

調整動畫使用的屬性

讓動畫變更變形(transform)和透明度(opacity)的屬性變更,即可避免版面配置和繪製,只需要合成。並且,變形和透明度必須升階於同一層,點此看範例。

管理圖層數目

只將必要的元素升階,這是因為每一圖層資訊都必須上傳至 GPU(註三),所以 CPU 和 GPU 的頻寬及 GPU 資訊的可用記憶體,都存在著限制。過度使用可能招致反效果,要謹慎使用。

效能評估

執行 Performance 的錄製,記錄在合成階段的資訊,例如合成所需要的時間。 合成花費時間

檢視合成的圖層數,位於 Chrome DevTools > More tools > Layers,如下圖。依此查看圖層數目、建立的原因和花費時間,盡量控制在 4 ~ 5ms。

檢視合成的圖層數

範例-模擬選單開合的 UI

改善 Composite 前

原始狀況,使用 display 來實作選單和內容開合,看起來不流暢且效能較差。

範例-改善 Composite 前

FPS meter

查看 fps 的方法,打開 Chrome DevTools > More tools > Rendering,勾選「FPS meter」,會出現以下視窗,看更多-Analyze frames per second (FPS)

FPS meter

改善 Composite 後

改用 transform 來實作選單和內容開合,看起來流暢多了,並且效能較好。

範例-改善 Composite 後

Threshold

若動畫必須控制在 10ms,那麼每個階段可花的時間是多少呢?(註五)

Threshold

圖片來源:Making a Silky Smooth Web

More

針對像素管道的四個階段,我將一些細部內容整理在此

備註

推薦閱讀

參考資料


關鍵轉譯路徑 轉譯效能 效能調校 Critical Rendering Path Rendering Performance Chrome DevTools Layout Thrashing Forced Synchronous Layout css3 animations css will-change flexbox BEM 前端效能 系列文