Web
How browser render a page
1. Navigation
- 用戶輸入 URL (或是點擊連結等)
- browser 執行 DNS lookup 來獲取對應的 IP address
- 如果之前有存取過該網頁,有可能會直接從 cache 拿就不用 DNS lookup
- 如果同一個網頁內的不同資源 (e.g. 字體、圖片) 來自不同 host,則 browser 也會對這些做 DNS lookup
- browser 對該 IP 做 TCP handshake (3 round trips)
- HTTPS 則需要再多做一個 TLS negotiation,用來決定加密使用的密碼 (5 round trips)
2. Response
- browser 傳送 initial HTTP GET request
- 這個 response 只會包含整個 HTML file 的 first byte (通常是 14 KB)
- Time to First Byte (TTFB) 的計算方式就是
responseStart - navigationStart
接下來的步驟又稱為 Critical Rendering Path (CRP): DOM → CSSOM → Render Tree → Layout → Paint
3. Parsing
當收到 first byte 後,main thread 就會開始逐行解析 HTML file 並根據情形決定要執行以下四種 task 的哪一個:
- html parsing
- css parsing
- javascript execution
- event handler
雖然此時 browser 還不一定有完整的 HTML 檔案,不過提前渲染可以減少用戶等待的時間,因此在前 14KB 就提供必要的 CSS 和 HTML 對效能優化很重要
現代的瀏覽器架構 (e.g. chromium) 都是 multi-process 的,不過在單一個 tab 上 (也就是一個網頁) 仍只會有一個 renderer process (main thread) 來進行渲染,所以同時間只能執行一個 task
HTML Parsing
- 如果碰到的是一般的 html tags,就會將該 html elements 解析成 node 之後加入 DOM tree 裡面
CSS Parsing
- 當遇到一個
<style>
或是<link rel="stylesheet" />
的時候,main thread 就會載入該 CSS 片段並解析- 如果是 external 的 stylesheet,則 main thread 會將下載的執行交給其他 thread,等到下載完成後才會通知 main thread 來執行 CSS Parsing
- 接著會將解析出來的 nodes “疊加” 到 CSSOM 內,為什麼說是 “疊加” 是因為後面的 rules 可能會覆蓋到前面的 rules
- 所以在還沒有把整個 CSSOM 建構出來前,都不會執行下一步 (render)
- 另外,Javascript 如果有關於 style 相關的操作,也會 block 直到整個 CSSOM 被建構出來
Javascript Execution
- 當遇到
<script>
時,main thread 就會下載並執行該 javascript 程式 (blocking) - 如果有
defer
屬性,下載會變成 non-blocking 而下載完後會等到整個 DOM 都解析完才執行 - 如果有
async
屬性,下載則會變成 non-blocking 而等到下載完成時會直接中斷當前 task 來執行- 也就是說執行的時候 DOM 可能還沒完全解析完,如果有操作 DOM 的話很容易出錯
- 而且也無法保證 script 之間的執行順序
Preload Scanner
- 其實大部分的解析時間都不會很長,反而是下載資源的時間佔了很大部分,因此就有了 preload scanner 來減少等待資源下載的時間。
- 當 main thread 在逐行解析 HTML 時,preload scanner 會同步執行來查看文件內有
rel="preload"
的資源,並做 non-blocking 的下載
4. Render
4-1. Style (Render Tree)
- 將 DOM 和 CSSOM 做結合並生成 render tree
- render tree 只會包含 visible content
- 也就是說
<head>
裡面的內容通常不會被包進去 display: none;
和它的子元素也不會
- 也就是說
- render tree 的重建會發生在 DOM 或 CSSOM 的結構改變時
4-2. Layout
- browser 會根據 render tree 的每個元素的位置、尺寸等
- 第一次決定每個元素的位置叫做 layout;後續對部分元素做更新叫做 reflow
- reflow 會發生在元素的位置、尺寸改變時,舉例來說:
- viewport 改變時 (因為
vw
等依賴 viewport 的屬性需要重算,flexbox 和 grid 也要重排) - 文字內容改變時 (因為可能會影響元素的尺寸)
- viewport 改變時 (因為
4-3. Paint
- 現在知道了每個元素的內容、style、位置、尺寸,接著 browser 就會把每個元素都繪製到螢幕上
- 繪製又被稱為 rasterization (柵格化),也就是將向量圖形格式轉換成可以顯示在螢幕上的點陣圖
- 第一次繪製出「與背景顏色不同的 pixels」的時間點叫做 First Paint (FP)
- 在 FP 之後對部分元素做重新繪製叫做 repaint
- repaint 會發生在元素的外觀等不影響 layout 的屬性改變時
Composite
- 在 paint 的時候,browser 會決定哪一些元素需要獨立出來形成一個新的 compositing layer 內
- 這些 compositing layer 的元素會使用 GPU 來繪製,而不會占用到 CPU 的資源,而且更快
- 像是有
transform
,opacity
屬性的元素、或是z-index
不同的元素 - 或是有
will-change
的元素一般而言,browser 在 compositing layer 不用後就會刪除來節省 memory,不過使用
will-change
會告訴 browser 哪些元素會(頻繁)改變,因此 browser 就會維持該元素的 compositing layer 來隨時使用,所以如果毫無節制的使用will-change
反而會大量占用 memory - 具體怎麼分層還是看 browser 本身的優化方式
- 有了這些 compositing layer,在處理某些 scrolling 或 animation 的時候就不需要對整個頁面做 reflow, repaint,而是針對特定 layer 來 repaint 就好,可以有效的提升性能
- 當有 compositing layer 時,就需要執行 composite 來組合不同的 layer 並確保順序是正確的
每次重做前面的步驟後,後面的也要重做,也就是說 reflow 後也要 repaint 和 re-composite,因此在確保用戶體驗上,每次重新 render (從 style 到 repaint 完成) 的過程不能超過 16.67ms
5. Interactivity
- 前面有提到,如果 script 有
defer
屬性,則會等到 DOM 解析完後才會開始執行,這時候如果頁面已經 paint 完成,也就是說用戶可以看到畫面了,但由於 main thread 忙著執行 script,沒有辦法處理用戶的 interaction event,變成頁面卡死的情形 - Time to Interactive (TTI) 是從第一個 request 到頁面真正可以互動的時間
Reference
- https://developer.mozilla.org/en-US/notes/Web/Performance/How_browsers_work
- https://developer.mozilla.org/en-US/notes/Web/Performance/Critical_rendering_path
- https://blog.jgerard.dev/deep-dive-into-the-browsers-rendering-pipeline-4c88c91f7bdc
- https://webperf.tips/tip/browser-rendering-pipeline/
- https://alex-ian.me/will-change
- https://developer.chrome.com/blog/inside-browser-part1#cpu_gpu_memory_and_multi-process_architecture (非常推薦)