從接收 HTML、CSS、JavaScript 到實際繪製第一個畫面,瀏覽器中間做了哪些事?什麼會卡住瀏覽器繪製頁面?理解這條路徑是效能優化的根本。

建構視角的對應章節:瀏覽器渲染流程

概覽#

瀏覽器從拿到檔案到畫出像素的整套流程稱為關鍵渲染路徑(Critical Rendering Path, CRP)。本文聚焦在 CRP 的觀念、如何估算載入頁面所需的關鍵成本,以及如何降低它來加速首次繪製。

兩個衡量指標#

分析頁面載入的 CRP 時可以參考:

  • 關鍵資源數(Critical Resources):開始顯示頁面前必須解析完畢的檔案數量。
  • 最短關鍵路徑(Minimum Critical Path):估算關鍵資源載入時間用的最少 round trip 次數。發出請求到收到回應稱為一次 round trip,多個資源同時下載仍視為一次。

從載入到開始互動大致經過:

  • 載入 HTML
  • 開始解析 HTML
  • 載入並解析其他檔案
  • 繼續解析 HTML,重複上述步驟
  • 全部解析完畢,開始轉譯頁面

範例拆解#

以下面這份 index.html 為例:

<html>
  <head>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div>Hello World!</div>
    <img src="An awesome image.jpg" />
    <script src="index.js"></script>
  </body>
</html>

開始轉譯前的流程:

  • 解析至第 3 行:開始載入 style.css(阻擋轉譯)
  • 解析至第 7 行:把圖片載入排入排程
  • 解析至第 8 行:開始載入 index.js(阻擋解析)
  • style.css 載入完成、開始解析
  • index.js 載入完成,等 CSS 解析完才執行
  • HTML 解析完成、開始轉譯

index.htmlstyle.cssindex.js 都必須處理完才能開始畫,因此關鍵資源為 3;CSS 與 JS 可同時下載,所以最短關鍵路徑是 2。

阻擋的兩種類型#

解析過程中會遇到 JavaScript、CSS、圖片等檔案,部分會造成 Render Blocking 或 Parser Blocking。

Render Blocking#

瀏覽器需要 HTML 與 CSS 才能繪出完整頁面。如果 HTML 一解析完就先畫一次,等 CSS 完成再重畫,使用者會看到一閃而過的無樣式畫面,也就是 Flash of Unstyled Content (FOUC)。為了避免,瀏覽器在解析完 CSS 前不會開始轉譯,CSS 過大或下載過久都會延後首次繪製。

Parser Blocking#

JavaScript 能新增或修改已解析的 DOM 元素,甚至插入 <link><script>,因此遇到 <script> 時,瀏覽器會把主執行緒交給 JavaScript 引擎執行(停止解析 HTML)。又因為 JavaScript 也會修改樣式,所以瀏覽器會等 CSS 解析完才執行 JavaScript,這也是為什麼常見把 <script> 放在文件最下方。

解除阻擋的策略#

降低檔案大小#

透過壓縮、tree shaking、code splitting 等手法縮小關鍵資源,減少下載與解析時間。

Inline#

把首屏所需的關鍵 CSS、JavaScript 直接寫進 HTML,省掉一次 round trip。

加上屬性提示#

例如行動裝置版的 CSS <link> 加上 media 屬性,避免桌機載入時被不必要的 CSS 阻擋;當 <script> 與 HTML、CSS 沒有直接關聯時,加上 asyncdefer 就不會阻止解析。

提早並行下載#

由於下載常常是最耗時的階段,盡可能讓關鍵資源越早開始下載越好;可借助 Lighthouse 找出 Critical Request Chains。

範例對照#

以下範例都在 Slow 3G 條件下,可比較關鍵資源與關鍵路徑的差異。

無 CSS 與 JavaScript#

<!DOCTYPE html>
<html>
  <head>
    <title>No CSS No JavaScript</title>
  </head>
  <body>
    <img src="doge.png" />
    <h1 class="main-title">No CSS & No JavaScript</h1>
  </body>
</html>

解析完 HTML 後馬上觸發 DOMContentLoaded 並繪出畫面,圖片載完後才觸發 onload

  • 關鍵資源:1
  • 最短關鍵路徑:1

有 CSS、無 JavaScript#

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="style.css" />
    <title>CSS & No JavaScript</title>
  </head>
  <body>
    <img src="doge.png" />
  </body>
</html>

<link> 的下載不會阻止 HTML 解析,但繪製要等 CSS 解析完成。

  • 關鍵資源:2
  • 最短關鍵路徑:2

有 CSS 與 JavaScript#

加入 <script src="index.js"></script> 後,因為 Parser Blocking,必須等 CSS 解析完並執行完 JavaScript 才觸發 DOMContentLoaded

  • 關鍵資源:3
  • 最短關鍵路徑:2

CSS 加上 async JavaScript#

<script> 加上 async,HTML 解析完即可立即觸發 DOMContentLoaded,JavaScript 不再阻擋解析。

  • 關鍵資源:2
  • 最短關鍵路徑:2

Non-blocking CSS#

<link rel="stylesheet" href="print.css" media="print" /> 會被視為較低優先權的請求,且不會阻止 JavaScript 執行。

  • 關鍵資源:2
  • 最短關鍵路徑:2

結語#

降低關鍵資源數與最短關鍵路徑是加速首次繪製最直接的辦法,開發時應檢查是否引入了不必要的等待,並用對應的 unblocking 技巧減輕負擔。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10248286