Optimizing Assets (preload/fetch/dns-prefetch/preconnect)

使用 preload、prefetch、dns-prefetch、preconnect 最佳化資源載入順序!

前言

先前面試時面試官詢問到我早期製作的一個 SPA 網站在換頁後圖片載入體驗不太好的問題,當時我把腦袋能想的所有答案都提出來了 😅,最終有答到一點邊緣知識,但太少用所以回答的不是很確定,當時回答:

  • 圖片應設定寬高以避免 CLS (累積布局偏移)
  • 圖片可以添加 Skeleton 或 Loader 示意加載中
  • 首先顯示較小的圖,適當時機再替換實際大張的圖
  • 圖片可以使用 fetchpriority 安排資源載入優先程度
  • 圖片內嵌使用 inline SVG 或 data URI
  • 使用 Preload 預先加載關鍵圖片……?

因此這次來補齊這方面瀏覽器提供的機制,主要介紹瀏覽器如何處理安排資源的加載。<head> 通常是 HTML 加載資源的區塊,其中 <link> 有特殊的 rel 屬性可以標示外部資源應當如何載入。

問題:載入順序時機

網頁圖片只有在載入頁面時才會載入,因此換頁時一些關鍵圖片沒有顯現出來會顯得很怪異。排開直接內嵌資源的選項有沒有方式可以改變頁面的資源加載順序以提高用戶體驗呢?

了解 rel="preload"

讓你在該頁面載入時預先聲明獲取資料的請求,通常是希望頁面載入早期就能立即加載的資料:

  • CSS 內部指向的資源如:字體或圖片
  • JavaScript 請求的資源,如 JSON
  • 各類體積較大的資源
  • ……

舉例使用 Google Fonts 時會拿到一份所有字體 @font-face 規則的 CSS 檔內指向字體的下載位置,這樣就不用一次載入整包字體檔案,瀏覽器會根據頁面上出現的文字與這份檔案中的 unicode-range 做比對,再去下載相對應的字體子集檔案:

/* latin */
@font-face {
font-family: 'Lato';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

而如果想要「預先下載特定資源」就可以標示 preload 以及類型 as="font" 讓特定字體檔案事先下載。

<link rel="preload" href="https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXg.woff2" as="font" crossorigin />

其他種類的 Preload 範例:

<link rel="preload" href="/style/other.css" as="style" />
<link rel="preload" href="/assets/font.woff2" as="font" />
<link rel="preload" href="/img/header.png" as="image" />

了解 rel="prefetch"

相較於 rel="prefetch" 專注於最佳化當頁內容的加載,preload 主要用在「下載下一頁可能會用到的資源」,最明顯的用途就是事先加載下一個用戶可能感興趣的頁面。這樣一來用戶就不用在換頁時及時加載下一頁:

<link rel="prefetch" href="next.html" />

由於載入內容攸關下一頁的體驗並不是非常的急迫,因此會等瀏覽器閒置時才執行該操作。適合換頁或彈窗才會出現的不緊迫資源使用。

其他種類 Prefetch 範例:

<link rel="prefetch" as="script" href="/date-picker.js">
<link rel="prefetch" as="style" href="/date-picker.css">

了解 rel="dns-prefetch"

相較於 「預先下載特定資源」 你也可以預先解析目標網址 DNS 進而加快請求發生時的速度。

<link rel="dns-prefetch" href="https://example.com/" as="document" />

了解 rel="preconnect"

告訴瀏覽器你想要預先連線到哪個網站,通常是稍後資源會用到的網域,讓瀏覽器事先建立連線從而減少延遲。

<link rel="preconnect" href="https://example.com/" crossorigin />

dns-prefetch vs preconnect

dns-prefetcpreconnect 之間的差異在於 dns-prefetch 只會執行 DNS 查找,而 preconnect 將執行 DNS 查找、TLS 協商和 TCP 握手。這意味著一旦資源準備好下載,就可以避免額外的 2 回來回通訊延遲。大致來說 preconnect 做更多準備且瀏覽器支援較差。

總結

  • preload
    • 用途:告訴瀏覽器目前頁面稍後會用到的資源。
    • 優先順序:較高,瀏覽器會盡快載入。
    • 使用場景:適合預載目前頁面關鍵資源,例如首屏圖片、關鍵 CSS、關鍵 JavaScript。
  • prefetch
    • 用途:告訴瀏覽器下一頁可能會用到的資源。
    • 優先順序:較低,瀏覽器會在閒置時才載入。
    • 使用場景:適合預先載入使用者可能點擊的連結頁面資源,例如文章列表頁預載文章內容。
  • dns-prefetch
    • 用途:告訴瀏覽器可能會連結的網址。
    • 使用場景:預先解析 DNS 位址,加快載入速度。
  • preconnect
    • 用途:告訴瀏覽器你想要預先連線到哪個網站。
    • 使用場景:預先建立連線,減少資源載入時的延遲。

須留意載入和執行是獨立的行為,載入後可以設定執行採用的動作:

樣式執行範例:

<link
rel="preload"
as="style"
onload="this.rel = 'stylesheet'"
href="https://fonts.googleapis.com/css?family=Roboto:100,900|Material+Icons"
/>
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:400,600|Material+Icons" />
</noscript>

腳本執行範例:

<link rel="preload" href="used-later.js" as="script" />
<!-- ... -->
<script>
const usedLaterScript = document.createElement('script');
usedLaterScript.src = 'used-later.js';
document.body.appendChild(usedLaterScript);
</script>

延伸閱讀