發布日期:2014 年 3 月 31 日
要找出及解決重要的轉譯路徑效能瓶頸,必須瞭解常見陷阱。您可以透過導覽瞭解常見的成效模式,進而改善網頁。
只要最佳化關鍵轉譯路徑,瀏覽器就能盡可能快速繪製網頁:網頁載入速度越快,使用者參與度就越高,瀏覽的網頁數量也會增加,轉換率也會提升。為了盡可能縮短訪客觀看空白畫面的時間,我們需要最佳化載入的資源和載入順序。
為了說明這個程序,請從最簡單的情況開始,然後逐步建構頁面,加入其他資源、樣式和應用程式邏輯。在這個過程中,我們會針對各個個案進行最佳化;也會看到哪裡出了問題
目前,我們只著重在資源 (CSS、JS 或 HTML 檔案) 可處理後,瀏覽器會發生什麼事。我們忽略從快取或網路擷取資源所需的時間。我們假設以下情況:
- 對伺服器的網路往返 (傳播延遲時間) 為 100 毫秒。
- HTML 文件的伺服器回應時間是 100 毫秒,其他檔案則是 10 毫秒。
Hello World 體驗
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
請先使用基本的 HTML 標記和單一圖片,不要使用 CSS 或 JavaScript。接著,請在 Chrome 開發人員工具中開啟「Network」面板,並檢查產生的資源瀑布流:
如預期,HTML 檔案的下載作業大約需要 200 毫秒。請注意,藍線的透明部分代表瀏覽器在網路上等待的時間長度,且不會收到任何回應位元組,而實心部分則顯示收到第一個回應位元組後,完成下載所需的時間。HTML 下載內容很小 (小於 4K),因此我們只需要單一往返傳輸即可擷取完整檔案。因此,HTML 文件的擷取時間大約需要 200 毫秒,其中一半的時間在網路等待,另一半則等待伺服器回應。
HTML 內容可供使用時,瀏覽器會剖析位元組、將其轉換為符記,並建構 DOM 樹狀結構。請注意,DevTools 會在底部方便地回報 DOMContentLoaded 事件的時間 (216 毫秒),這也對應到藍色垂直線。HTML 下載結尾與藍色垂直線 (DOMContentLoaded) 之間的時間差,即為瀏覽器建立 DOM 樹狀結構所需的時間,只有幾毫秒。
請注意,「超棒相片」並未封鎖 domContentLoaded
事件。結果證明,我們可以建構轉譯樹狀結構,甚至無須等待網頁上的每個素材資源,即可繪製網頁:並非所有資源都必須在首次顯示畫面時呈現。事實上,當我們談論關鍵運算路徑時,通常指的是 HTML 標記、CSS 和 JavaScript。圖片不會阻礙網頁的初始轉譯作業,但我們也應盡快繪製圖片。
也就是說,圖片上的 load
事件 (也稱為 onload
) 已遭封鎖:開發人員工具會在 335 毫秒時回報 onload
事件。請注意,onload
事件會標示網頁所需的所有資源已下載及處理的時間點;此時,載入旋轉圖示可停止在瀏覽器中旋轉 (圖表中的紅色垂直線)。
將 JavaScript 和 CSS 加入混合
我們的「Hello World 體驗」頁面看起來雖然很基本,但其實很多。實際上,除了 HTML 外,我們還需要的不只是 HTML:有時候我們會提供 CSS 樣式表和一或多個指令碼,以便為網頁增添互動性。將兩者加入混合,看看會發生什麼事:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Script</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="timing.js"></script>
</body>
</html>
新增 JavaScript 和 CSS 前的注意事項:
使用 JavaScript 和 CSS:
新增外部 CSS 和 JavaScript 檔案會在刊登序列中加入兩個額外的請求,瀏覽器大約會同時分派這些請求。但請注意,domContentLoaded
和 onload
事件的時間差異現在比現在小很多。
為什麼會這樣?
- 與純 HTML 範例不同的是,我們也必須擷取並剖析 CSS 檔案以建構 CSSOM,同時需要 DOM 和 CSSOM 來建構算繪樹狀結構。
- 網頁也包含會封鎖 JavaScript 檔案的剖析器,因此在下載並剖析 CSS 檔案之前,
domContentLoaded
事件會遭到封鎖:因為 JavaScript 可能會查詢 CSSOM 檔案,所以在下載 CSS 檔案之前,我們必須將其封鎖,系統才能執行 JavaScript。
如果將外部指令碼替換為內嵌指令碼,該怎麼辦?即使指令碼已直接內嵌至網頁,瀏覽器仍必須先建構 CSSOM 才能執行指令碼。簡單來說,內嵌 JavaScript 也是剖析器封鎖功能。
不過,即使 CSS 遭到封鎖,內嵌指令碼是否能讓網頁顯示速度加快?試試看,看看會有什麼結果。
外部 JavaScript:
內嵌 JavaScript:
我們會減少一項要求,但 onload
和 domContentLoaded
次實際上是相同的。這是因為我們知道,無論 JavaScript 是否內嵌或外部,都不重要,因為當瀏覽器點擊封鎖的指令碼標記後,並等到 CSSOM 建立完成。此外,在第一個範例中,瀏覽器會同時下載 CSS 和 JavaScript,並在幾乎同時完成下載。在此情況下,內嵌 JavaScript 程式碼對使用者沒有什麼幫助。不過,有多種策略可加快網頁轉譯速度。
首先,請回想所有內嵌指令碼都是解析器阻擋,但對於外部指令碼,我們可以新增 async
屬性來解除阻擋解析器。取消內嵌,然後試試看:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Async</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body onload="measureCRP()">
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script async src="timing.js"></script>
</body>
</html>
剖析器封鎖 (外部) JavaScript:
非同步 (外部) JavaScript:
好多了!HTML 剖析 HTML 後不久就會觸發 domContentLoaded
事件;瀏覽器知道不要在 JavaScript 上封鎖,而且因為沒有其他剖析器會封鎖 CSSOM 建構程序,所以也能夠平行處理。
或者,我們也可以將 CSS 和 JavaScript 內嵌:
<!DOCTYPE html>
<html>
<head>
<title>Critical Path: Measure Inlined</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
</style>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script>
var span = document.getElementsByTagName('span')[0];
span.textContent = 'interactive'; // change DOM text content
span.style.display = 'inline'; // change CSSOM property
// create a new element, style it, and append it to the DOM
var loadTime = document.createElement('div');
loadTime.textContent = 'You loaded this page on: ' + new Date();
loadTime.style.color = 'blue';
document.body.appendChild(loadTime);
</script>
</body>
</html>
請注意,domContentLoaded
時間與上一個範例相同。我們在網頁本身中內嵌 CSS 和 JS,而非將 JavaScript 標示為非同步。這會使得 HTML 網頁的變大,但缺點是瀏覽器不必等待擷取任何外部資源。一切都沒問題
如您所見,即使擁有非常基本的網頁,最佳化關鍵轉譯路徑也是一件簡單的事:我們需要瞭解不同資源之間的依附關係圖,而我們必須找出哪些資源「關鍵」。我們必須選擇不同的策略,才能將這些資源納入網頁上。這個問題沒有一個解決方法;因為每個頁面的內容都不同您需要自行按照類似程序操作,才能找出最佳策略。
不過,我們可以先退一步,找出一些一般性的成效模式。
成效模式
最簡單的網頁只包含 HTML 標記,沒有 CSS、JavaScript 或其他類型的資源。如要算繪這個網頁,瀏覽器必須發出要求、等待 HTML 文件傳送到達、剖析該文件、建構 DOM,然後在畫面上算繪:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Critical Path: No Style</title>
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
T0 和 T1 之間的時間會擷取網路和伺服器處理時間。在最佳情況下 (如果 HTML 檔案很小),只需進行一次網路來回傳輸,即可擷取整份文件。基於 TCP 傳輸通訊協定的運作方式,大型檔案可能需要更多往返行程。因此,在最理想的情況下,上述頁面會有一趟來回 (最小) 的關鍵轉譯路徑。
現在請將外部 CSS 檔案視為同一個網頁:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
</body>
</html>
再次執行網路來回作業以擷取 HTML 文件,然後擷取的標記會告訴我們也需要 CSS 檔案;也就是說,瀏覽器必須先返回伺服器並取得 CSS,才能在畫面上轉譯網頁。因此,本頁面至少會產生兩次來回行程,必須才能顯示。再次強調,CSS 檔案可能需要多次來回傳輸,因此強調「最少」這個字眼。
以下是我們用來描述關鍵算繪路徑的幾個術語:
- 「重要資源」:可能會妨礙網頁初始轉譯的資源。
- 關鍵路徑長度:來回傳輸次數,或擷取所有關鍵資源所需的總時間。
- 重要位元組:取得網頁首個轉譯作業所需的總位元組數,也就是所有重要資源的傳輸檔案大小總和。第一個範例是單一 HTML 網頁,其中包含一項重要資源 (HTML 文件);重要路徑長度也等於一次網路來回行程 (假設檔案較小),而關鍵位元組總數只就等於 HTML 文件本身的傳輸大小。
現在,請比較先前 HTML 和 CSS 範例的關鍵路徑特性:
- 2 重要資源
- 2 以上來回傳輸,以達到最短關鍵路徑長度
- 9 KB 的重要位元組
我們需要 HTML 和 CSS 來建構轉譯樹狀結構。因此,HTML 和 CSS 都是重要資源:只有在瀏覽器取得 HTML 文件後,系統才會擷取 CSS,因此重要路徑長度至少為兩次往返。這兩項資源的總量總和高達 9 KB。
接著將額外的 JavaScript 檔案新增到組合中。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js"></script>
</body>
</html>
我們新增了 app.js
,這既是網頁上的外部 JavaScript 資產,也是剖析器封鎖的資源 (重要) 資源。更糟的是,為了執行 JavaScript 檔案,我們必須阻斷並等待 CSSOM;請注意,JavaScript 可以查詢 CSSOM,因此瀏覽器會暫停,直到 style.css
下載並建構 CSSOM 為止。
實際上,我們要查看這個網頁的「聯播網刊登序列」您會發現 CSS 和 JavaScript 請求的發出時間差不多;瀏覽器取得 HTML 後,會發現這兩項資源,並發出這兩項要求。因此,上圖所示的網頁具有下列關鍵路徑特性:
- 3 項重要資源
- 關鍵路徑長度最短的往返次數:2 次以上
- 11 KB 的必要位元組
我們現在有三個重要資源,總共 11 KB 的必要位元組,但必要路徑長度仍為兩次往返,因為我們可以並行傳輸 CSS 和 JavaScript。找出關鍵運算路徑的特性,代表您能夠識別關鍵資源,並瞭解瀏覽器如何安排擷取作業。
與網站開發人員交流後,我們發現不需封鎖網頁中的 JavaScript。包含部分數據分析和其他程式碼
不需要阻擋網頁轉譯透過這些知識,我們可以將 async
屬性新增至 <script>
元素,以解除封鎖剖析器:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
非同步指令碼有幾項優點:
- 指令碼不再遭到剖析器封鎖,也不屬於關鍵轉譯路徑的一部分。
- 由於沒有其他重要指令碼,CSS 不需要封鎖
domContentLoaded
事件。 domContentLoaded
事件觸發越早,其他應用程式邏輯就能越快開始執行。
因此,經過最佳化的網頁現在回歸了兩項重要資源 (HTML 和 CSS),其中關鍵路徑長度最短為兩次往返,總計 9 KB 的關鍵位元組。
最後,如果 CSS 樣式表只需要用於列印,該看起來會怎麼樣?
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link href="style.css" rel="stylesheet" media="print" />
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg" /></div>
<script src="app.js" async></script>
</body>
</html>
由於 style.css 資源只用於列印,因此瀏覽器不需要封鎖該資源即可轉譯網頁。因此,當 DOM 建構完畢後,瀏覽器就會取得足以轉譯網頁的資訊。因此,這個網頁只有一個必要資源 (HTML 文件),且必要運算路徑的長度最短為一輪。