關鍵轉譯路徑 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

同理,由 CSS 產生的節點也會加入 Render Tree,如下所示,<h1:after> 這個節點也會新增到 <h1> 之下。

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, .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

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

備註

推薦閱讀

參考資料


comments powered by Disqus