關鍵轉譯路徑 Critical Rendering Path
13 Jul 2018假設畫面有任何變動,例如滾動捲軸而產生動畫效果,裝置將會更新螢幕。現今裝置更新畫面的頻率是每秒 60 幀(60Hz 或稱 60fps),意即每幀運行的時間最多是 16.67ms。但瀏覽器不僅要渲染畫面,還有很多事情要忙,因此每幀運行的時間只能約 10 ~ 12ms。若瀏覽器拖太久才更新畫面,就會產生顫動(Juddering)。想提高更新畫面的頻率、避免顫動,就要了解瀏覽器如何渲染畫面。
Browser Rendering Pipeline
瀏覽器渲染的過程可看成是一連串的步驟,也就是像素管道(Browser Rendering Pipeline)。
說明
- JavaScript:使用 JavaScript 觸發樣式變更,也可使用 CSS Animation 或 Web Animation API。
- Styles:找出哪些 CSS 規則適用於哪些元素,並計算每個元素的最終樣式。
- Layout:計算可視元素的版面配置,例如:width、height、margin、position。
- Paint:繪製物件像素圖層,例如:box-shadow、border-radius、color、background-color、text-shadow。這階段往往耗時最長、成本最高,應儘量避免。
- Composite:將圖層依序繪製到畫面上,例如:transform、opacity。
在渲染的過程中,不需每個環節都走過,可選用經過最少階段的指令,像是…
- 更新 width,走過的環節有 JavaScript/Styles -> Layout -> Paint -> Composite
- 只更新 background,走過的環節有 JavaScript/Styles -> Paint -> Composite
- 只更新 opacity,走過的環節有 JavaScript/Styles -> Composite
使用 CSS Triggers 來查詢指令會觸發和走過哪些階段。
範例
使用 flexbox 排列一連串的盒子,盒子從小變大。請問會走過哪些環節?Style?Layout?Paint?Composite?
圖片來源: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 移除
- 擁有樣式
display: none
的節點 - 不可見的標籤,像是
<head>
和<script>
但以下節點依然可見,所以仍會存在於 Render Tree
height: 0
(註一)position: absolute; left: 100%
(註二)visibility: hidden
- 含有 pseudo class
p::before{content:"Hi!"}
的內容,但這部份在 DOM 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 就可能會發生顫動。
版面配置(Recaculate Layout / Reflow)
在樣式計算的階段,瀏覽器知道什麼樣的 CSS 規則要套用到哪一個 DOM Element 後,接著就可以開始計算每個可視元素的版面配置,這個分配元素位置的過程稱為 Recaculate Layout 或 Reflow,在 Chrome DevTools 上稱為 Layout。
如何提升版面配置的效能?
影響元素大小或位置的指令會導致瀏覽器做 Reflow,例如:width、height、margin、position 等。Layout 的作用範圍通常是整個 Document,若希望減少 Reflow 的成本,可將 Reflow 的範圍縮小在特定 DOM Element 內,例如使用能限制範圍的元素。
能限制範圍的元素有以下特性
- SVG root
(<svg>)
- 文字或搜尋框
<input>
- 非 inline 或 inline-block 元素
- 高度值非使用百分比,例如:可以是
height: 100px
- 高度值非瀏覽器自行運算或預設值,例如:不可以是
height: auto
- 寬度值非瀏覽器自行運算或預設值,例如:不可以是
width: auto
- overflow 的值是明確設定而非預設值,例如:可以是 scroll、auto、hidden
- 非
<table>
的後代元素
或從工具查看可改善的地方
- Chrome DevTools Timeline,點此看範例
- Paul Lewis’ Boundarizr,這個工具會將可當成 Layout Boundary 的 DOM Element 標記出來,點此看範例
最後可從 Layout Scope、影響的 Node 數和經歷的 Duration 等方面來檢視改善效果。
除了縮小 Reflow 範圍外,也可從以下方式來減少成本
- 減少版面配置的元素數量
- 降低樣式規則的複雜度,愈簡單的規則,愈容易在 Render Tree 上查找,所花的時間就愈少。
- 降低版面配置的複雜性
- 避免強制同步版面配置(Forced Synchronous Layout)。發生的原因是由於在 JavaScript 程式碼中觸發 Layout 階段的 CSS 指令,例如:讀取某個元素的 offsetWidth 值,就會強迫瀏覽器在此幀(Frame)就必須更新,導致瀏覽器馬上計算樣式和配置版面,接著更新樣式,使得瀏覽器進入讀取/寫入的循環,這裡有圖文並茂的說明。
- 避免快速連續版面配置(Layout Thrashing)。若不斷地 Forced Synchronous Layout,就會導致 Layout Thrashing,這裡還有圖文並茂的說明。這樣連續的過程非常耗效能。可用 FastDOM 解決,FastDOM 會自動批次完成讀 / 寫操作,避免意外觸發強制同步版、快速連續版面配置。
圖片來源:Making a Silky Smooth Web
範例 1
如何使用 Chrome DevTools Timeline 檢視 Layout Scope?點此看 Demo,使用者可對整個頁面做點擊,接著依序更改特定元素的寬度
- 第一次點擊會更改
<body>
的寬 - 第二次點擊會更改
<div id="d1">
的寬 - 第三次點擊會更改
<div id="d2">
的寬
因此,更改哪一個區塊的寬,會讓瀏覽器花最多的工來 Reflow 畫面?
- A:更改
<body>
的寬 - B:更改
<div id="d2">
的寬 - C:以上兩者所花的工是相同的
答案是 C。
檢視 Chrome DevTools 的 tab「Performance」,錄製整個過程-點擊畫面三次,發現都會更新整個網頁(document),因為更新範圍,即 Layout Scope 相同,推知瀏覽器 Reflow 所花的工是相同的。
第一次點擊會更改 <body>
的寬。
點擊「Layout」這個紫色的區塊,瀏覽器會將 Layout Scope 用陰影標記出來。
第三次點擊會更改 <div id="d2">
的寬。
範例 2
如何使用 Boundarizr 檢視 Layout Scope?
在要檢視的頁面載入這個 library 後,若頁面的 DOM Element 可當作 Layout Boundary,就會用陰影的方式標記出來,例如以下這個 <input>
。
<input type="text" placeholder="Hello World" />
若要取消標記,點擊上方按鈕「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-change
或 translateZ(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」,當發生繪製的時候,發生之處會閃綠光。可依此判斷此處是否需要繪製來做調整。
Timeline
執行 Performance 的錄製,記錄在繪製階段的資訊,例如繪製所需要的時間。
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 來實作選單和內容開合,看起來不流暢且效能較差。
- 更新頻率:1 fps
- 經過 Styles (120μs) > Layout (220μs) > Paint (140μs) > Composite (220μs)
- 程式碼與 Demo
FPS meter
查看 fps 的方法,打開 Chrome DevTools > More tools > Rendering,勾選「FPS meter」,會出現以下視窗,看更多-Analyze frames per second (FPS)。
改善 Composite 後
改用 transform 來實作選單和內容開合,看起來流暢多了,並且效能較好。
- 更新頻率:12 - 20 fps
- 經過 Styles (420μs) > Composite (200μs),比改善前少了 Layout 和 Paint 兩個階段,這是因為 transform 只會影響 Composite。
- 程式碼與 Demo
Threshold
若動畫必須控制在 10ms,那麼每個階段可花的時間是多少呢?(註五)
- 樣式計算(Style Calculations)要在 1ms 以內
- 版面配置(Layout)要在 3ms 以內
- 圖層管理(Layer Management)要在 2ms 以內
- 合成(Composite)要在 2ms 以內
圖片來源:Making a Silky Smooth Web
More
針對像素管道的四個階段,我將一些細部內容整理在此
備註
- 註一:
height: 0
無法保證不可見,範例 - 註二:
position: absolute; left: 100%
無法保證不可見,範例 - 註三:Browser Rendering Pipeline 的過程是在 CPU 運作,若支援 GPU,也就是硬體加速,會放到 GPU 做合成,並顯示於螢幕上。
- 註四:block-like flows 與 inline flows 的差異,分別導致有 single-pass(由左到右、由上到下) 和 multi-pass (計算 X 軸和 Y 軸和順逆排序)的 codepaths。
- 註五:若 Browser Rendering Pipeline 要符合 RAIL,每個過程所必須必須達到門檻值。
推薦閱讀
參考資料
- The Critical Rendering Path
- Introducing ‘layout boundaries’
- Simplify Paint Complexity and Reduce Paint Areas
- Stick to Compositor-Only Properties and Manage Layer Count
- Reduce the Scope and Complexity of Style Calculations
- Performance Analysis Reference
- Avoid Large, Complex Layouts and Layout Thrashing
- Making a Silky Smooth Web