preload、preconnect、prefetch:資源提示的正確用法
資源提示(Resource Hints)讓瀏覽器 提早開始處理 某個資源。它們是改善 LCP 的常用工具,但也是最容易用錯、反而拖慢載入的一組。
四種提示各解決什麼
| 提示 | 解決什麼問題 | 針對 |
|---|---|---|
preload | 資源 被發現得太晚 | 當前頁面 |
preconnect | 跨網域的 連線交握太慢 | 當前頁面 |
dns-prefetch | 只要省下 網域解析 | 當前頁面 |
prefetch | 預測 下一個頁面 會用到 | 下一個頁面 |
先問「問題是什麼」
不要因為看到別人用就照抄。每一種提示都對應一個具體的問題,沒有那個問題就不必用,用了只會浪費頻寬。
preload:資源被發現得太晚
瀏覽器要 讀到 資源的宣告才會開始下載。有些資源藏得很深:
| 藏在哪 | 為什麼晚 |
|---|---|
CSS 的 background-image | 要等 CSS 下載並解析完 |
CSS 的 @font-face | 同上,還要算出有元素用到它 |
| JavaScript 動態插入的資源 | 要等程式碼執行 |
CSS 裡 @import 的檔案 | 要等外層 CSS 解析完 |
<!-- 首屏的背景圖:提早開始下載 -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<!-- 首屏的字型 -->
<link rel="preload" as="font" type="font/woff2" href="/fonts/main.woff2" crossorigin>as 是必填
as 告訴瀏覽器這是什麼資源,影響它的優先權與請求標頭:
| 值 | 用於 |
|---|---|
image | 圖片 |
font | 字型 |
style | CSS |
script | JavaScript |
fetch | 用 fetch 取得的資料 |
video、audio | 影音 |
沒寫 as 會下載兩次
瀏覽器無法判斷這是什麼資源時,會用不同的請求設定,結果是預載一次、實際使用時再下載一次,完全反效果。
字型必須加 crossorigin
這是最常見的 preload 錯誤
<!-- 錯誤:會下載兩次 -->
<link rel="preload" as="font" href="/fonts/main.woff2">
<!-- 正確 -->
<link rel="preload" as="font" type="font/woff2" href="/fonts/main.woff2" crossorigin>字型的請求一律以 匿名跨來源模式 發出(即使是同網域)。預載時不加 crossorigin,瀏覽器會認為那是兩個不同的請求。
只預載一到兩個
濫用 preload 會讓 LCP 變差
<!-- 反效果:十個資源互相搶頻寬 -->
<link rel="preload" as="font" href="/fonts/regular.woff2" crossorigin>
<link rel="preload" as="font" href="/fonts/bold.woff2" crossorigin>
<link rel="preload" as="font" href="/fonts/light.woff2" crossorigin>
<link rel="preload" as="image" href="/hero.webp">
<link rel="preload" as="image" href="/logo.svg">
<link rel="preload" as="image" href="/banner-1.webp">
<!-- ... -->頻寬是固定的。 預載會提高優先權去搶頻寬,預載十個等於讓它們互相競爭,真正重要的那一個反而變慢。
原則:整站只預載首屏一到兩個關鍵資源,通常是 LCP 圖片與一個字型檔。
preconnect:提早建立連線
跨網域的資源要先做 DNS 查詢、TCP 連線、TLS 交握,三段加起來可能上百毫秒。preconnect 提早把這三段做完。
<!-- 字型服務:提早建立連線 -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>字型檔的網域要加 crossorigin
fonts.googleapis.com 提供的是 CSS,fonts.gstatic.com 提供的是字型檔。後者要加 crossorigin,理由和 preload 相同。
| 適合 | 不適合 |
|---|---|
| 首屏用到的第三方字型服務 | 頁尾才載入的分析工具 |
| 首屏圖片的 CDN | 使用者可能不會觸發的第三方 |
| 關鍵的 API 網域 | 自己的網域(已經連上了) |
不要超過四個
每一個 preconnect 都會佔用資源並 保持連線一段時間(通常 10 秒),加太多會消耗頻寬與電力,行動裝置尤其明顯。
dns-prefetch:只省網域解析
比 preconnect 輕量,只做 DNS 查詢,不建立連線。
<link rel="dns-prefetch" href="https://analytics.example.com">preconnect | dns-prefetch | |
|---|---|---|
| 做什麼 | DNS + TCP + TLS | 只有 DNS |
| 成本 | 較高 | 很低 |
| 效益 | 較大 | 較小 |
| 適合 | 首屏關鍵的少數網域 | 稍後才會用到的較多網域 |
兩者可以並用當退路
<link rel="preconnect" href="https://cdn.example.com">
<link rel="dns-prefetch" href="https://cdn.example.com">不支援 preconnect 的環境會落到 dns-prefetch。現在支援度已經很好,這個寫法的必要性降低了,但寫了也沒壞處。
prefetch:為下一個頁面準備
它針對的是 下一個頁面,優先權很低,只在瀏覽器閒置時下載。
<!-- 使用者很可能接著看的頁面 -->
<link rel="prefetch" href="/seo/core-web-vitals-lcp">預測錯就是純浪費
使用者沒去那一頁的話,下載的流量完全白費,行動網路上這是實際的成本。
只在 有明確的下一步 時使用:結帳流程的下一步、多步驟表單、閱讀順序明確的教學系列。
多數框架的路由套件已經內建這個行為(例如:滑鼠移到連結上時才預先擷取),不必自己手寫。
modulepreload
ES 模組專用的預載,會連同它的依賴一起處理:
<link rel="modulepreload" href="/app.js">preload + as="script" | modulepreload | |
|---|---|---|
| 下載 | 是 | 是 |
| 解析 | 否 | 是 |
| 處理依賴 | 否 | 是(遞迴) |
建置工具通常自動處理
Vite 這類工具在建置時會自動插入 modulepreload,並為不支援的瀏覽器加上 polyfill。手動加的必要性很低。
fetchpriority:相對的優先權
它不是提示,而是 調整既有請求的優先權:
<!-- 首屏主視覺:提高 -->
<img src="/hero.webp" alt="主視覺" fetchpriority="high" width="1200" height="400">
<!-- 首屏之外的裝飾圖:降低 -->
<img src="/decoration.webp" alt="" fetchpriority="low" loading="lazy">| 值 | 意義 |
|---|---|
high | 比同類資源優先 |
low | 比同類資源延後 |
auto | 交給瀏覽器決定(預設) |
它是相對的,不是絕對的
全部資源都設成 high 等於沒設,互相比較的結果沒有變。
實務上 整頁只該有一到兩個資源設為 high,其餘維持 auto 或降為 low。
fetchpriority 也可以用在 preload 與 fetch() 上:
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">// 不重要的請求降低優先權,不要跟首屏資源搶
fetch('/api/analytics', { priority: 'low' });一份合理的組合
大多數網站需要的就這幾行:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 首屏用到的第三方字型服務(若有) -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 首屏的關鍵字型(只一個) -->
<link rel="preload" as="font" type="font/woff2" href="/fonts/main.woff2" crossorigin>
<title>頁面標題|網站名稱</title>
</head>
<body>
<!-- LCP 圖片直接寫在 HTML 裡,不需要 preload -->
<img src="/hero.webp" alt="主視覺" width="1200" height="400" fetchpriority="high">
</body>HTML 裡的 img 通常不需要 preload
瀏覽器的預載掃描器會提早發現 HTML 裡的 <img>。需要 preload 的是藏在 CSS 或 JavaScript 裡的資源。
直接寫在 HTML 的圖片只要加 fetchpriority="high" 就夠了。
這個站的做法
這個站 一個資源提示都沒有手寫,因為沒有需要解決的問題:
| 為什麼不需要 | 說明 |
|---|---|
| 不用網頁字型 | 用系統字型堆疊,沒有字型要預載 |
橫幅是 HTML 裡的 <img> | 預載掃描器自己會發現 |
| 橫幅是 SVG,只有幾 KB | 下載時間本來就很短 |
| 沒有第三方字型或圖片服務 | 沒有跨網域連線要提早建立 |
建置工具還是會自己加
檢視原始碼會看到 modulepreload 與樣式的 preload,那是 Vite 依實際的模組相依關係自動產生的,不是手寫的。
這正是本篇的原則:該由工具判斷的交給工具,人只在有具體問題時才手動加提示。
只有橫幅圖用了 fetchpriority="high":
{ width=1200 height=400 fetchpriority="high" }沒有問題就不要加提示
這是本篇最重要的一句。資源提示不是「加了就會更快」的東西,它是針對特定問題的工具,濫用會反過來拖慢。
檢查清單
| 項目 | 標準 |
|---|---|
| 每個提示都對應一個具體問題 | 必備 |
preload 不超過兩個 | 必備 |
preload 都有寫 as | 必備 |
字型的 preload 有 crossorigin | 必備 |
preconnect 不超過四個 | 建議 |
fetchpriority="high" 不超過兩個 | 必備 |
HTML 裡的 <img> 沒有多餘的 preload | 建議 |
prefetch 只用在有明確下一步時 | 建議 |
| 加了提示後有量測是否真的改善 | 建議 |
常見問題
四種提示怎麼選?
看您要解決哪個問題。資源被發現得太晚用 preload;跨網域連線的交握太慢用 preconnect;只要省下網域解析用 dns-prefetch;預測使用者下一步會去哪用 prefetch。前三者針對當前頁面,最後一個針對下一個頁面。
為什麼濫用 preload 會拖慢載入?
因為頻寬是固定的。預載的資源會提高優先權去搶頻寬,預載了十個資源等於讓它們互相競爭,真正重要的那一個反而變慢。原則是整站只預載一到兩個首屏關鍵資源。
fetchpriority 設成 high 就一定會先載嗎?
它是相對的優先權,不是絕對的。全部資源都設成高等於沒設,因為互相比較的結果沒有變。實務上整頁只該有一到兩個資源設為高,其餘維持預設或降為低。
preconnect 可以加幾個?
建議不超過四個。每一個預先連線都會佔用資源並保持連線一段時間,加太多反而消耗頻寬與電力。只對首屏真正需要的關鍵第三方網域使用。
字型預載需要 crossorigin 嗎?
需要,而且是必填。字型的請求一律以匿名跨來源模式發出,預載時若不加這個屬性,瀏覽器會認為是兩個不同的請求而下載兩次,完全失去預載的意義。