說出從「按下 Enter」到「第一個 HTML 位元組回來」之間,瀏覽器依序做了哪些事。
解釋 為何是網路效能的基本單位,並能估算一次新連線需要幾個 RTT。
指出 DNS、TCP、TLS、重導、Service Worker 啟動等環節各自可能慢在哪裡,以及對應的優化手段。
理解 HTTP/1.1、HTTP/2、HTTP/3(QUIC)在連線成本上的差異。
用 Performance API 與 DevTools Network 面板實際量出每個階段花了多久。
一個導覽(navigation)的完整流程
當使用者在網址列輸入 並按下 Enter,瀏覽器並不是立刻「fetch」。在真正送出 HTTP 請求之前,有一連串的準備工作;圖 1.1 把這些步驟依時間排開,並標出 Navigation Timing API 中對應的時間點屬性(會完整介紹這個 API)。
max width=
逐步說明:
卸載舊頁面:若目前分頁已有頁面,瀏覽器先觸發 、、。註冊 監聽器會讓頁面無法進入 bfcache(),是常見的效能反模式。
HSTS 與重導:瀏覽器先查 HSTS 清單,若該網域在清單中就直接升級為 HTTPS,省下一次 \(\rightarrow\) 的重導。接著若伺服器回傳 301/302/307/308,瀏覽器必須重新對新 URL 走一遍流程;每一次重導至少多一個 RTT,若換了網域還要重新做 DNS、TCP、TLS。
Service Worker:若此 URL 在某個已註冊的 Service Worker 控制範圍內,瀏覽器必須先啟動該 worker(),再把請求交給它的 事件處理。啟動時間在低階手機上可能達數百毫秒(會談 navigation preload 如何緩解)。
HTTP 快取檢查:若快取中有仍然新鮮(fresh)的回應,瀏覽器直接使用,完全不碰網路;此時 DNS/TCP/TLS 各時間點會等於 , 為 0。
DNS 查詢:把主機名稱換成 IP 位址。
TCP 三向交握:建立可靠傳輸通道(HTTP/3 改用 QUIC,沒有這一步)。
TLS 交握:協商加密參數、驗證憑證。
這之後才是大家熟悉的「送請求、等回應」。第一個位元組抵達的時間就是 。
「fetch 之前」的所有步驟都是純粹的等待:畫面上沒有任何東西可以畫。這段時間直接墊高 TTFB,進而墊高 FCP 與 LCP。要優化它,關鍵是理解每個步驟需要幾個 RTT,以及如何把這些往返「提前做」或「省下來」。
RTT:網路效能的基本單位
一個封包從客戶端送到伺服器、再由伺服器回到客戶端所需的時間。它主要由物理距離(光在光纖中的速度約為每毫秒 200 公里)、路由跳數與網路壅塞 / 排隊決定,與頻寬(bandwidth)幾乎無關。
幾個常見的量級(僅供直覺參考):
L6.5cm L4cm 情境 & 典型 RTT
同一城市內的有線寬頻到附近 CDN 節點 & 5–20 ms
台灣 \(\leftrightarrow\) 日本 / 新加坡 & 30–60 ms
台灣 \(\leftrightarrow\) 美國西岸 & 120–160 ms
4G 行動網路(訊號良好) & 40–100 ms
訊號不佳的行動網路 & 200–500 ms 以上
把 RTT 想成「寫信給遠方朋友、等他回信」的時間。建立連線就像雙方在正式談事情之前要先互寄幾封信確認身分、交換密碼本。信寄得再厚(頻寬再大)也沒用,重點是要來回幾趟。
建立一條新連線要幾個 RTT?
表 [tab:rtt-count] 比較不同協定組合下,從開始建立連線到收到第一個 HTTP 回應位元組所需的往返次數(不含 DNS,假設伺服器處理時間為零)。
L5.2cm C2.2cm C2.2cm C2.4cm C1.8cm 協定組合 & 傳輸層 & 加密層 & HTTP 請求/回應 & 合計
HTTP/1.1 或 HTTP/2,TCP + TLS 1.2(新連線) & 1 & 2 & 1 & 4 RTT
HTTP/1.1 或 HTTP/2,TCP + TLS 1.3(新連線) & 1 & 1 & 1 & 3 RTT
TCP + TLS 1.3 0-RTT 恢復(回訪) & 1 & 0(資料隨第一封加密訊息送出) & 1 & 2 RTT
HTTP/3,QUIC(新連線,TLS 1.3 內建) & & 1 & 2 RTT
HTTP/3,QUIC 0-RTT 恢復(回訪) & & 1 & 1 RTT
以 RTT = 100 ms 的行動網路為例:即使伺服器瞬間回應,TLS 1.2 的路徑也要 400 ms 才看到第一個位元組;改用 HTTP/3 則是 200 ms,回訪時更可縮到 100 ms。這就是為什麼「升級協定」本身就是一項效能優化。
max width=
各環節可能慢在哪裡、如何優化
重導(Redirect)
慢的原因:每次重導都至少多一個 RTT;跨網域的重導(例如 \(\rightarrow\) ,或 \(\rightarrow\) )還要重新做 DNS、TCP、TLS。行銷連結常見的「追蹤跳轉」可能連跳三、四次。
優化:
在所有內部連結、sitemap、廣告投放中直接使用最終 URL(含 、正確的 www 與結尾斜線)。
設定 HSTS()並申請加入瀏覽器的 HSTS preload 清單,讓瀏覽器連第一次造訪都直接走 HTTPS。
用 與 監控真實使用者遇到的重導成本。
DNS 查詢
慢的原因:DNS 查詢有多層快取(瀏覽器 \(\rightarrow\) 作業系統 \(\rightarrow\) 遞迴解析器 \(\rightarrow\) 權威伺服器)。全部未命中時需要多次往返,web.dev 的估計是常見約 20–120 ms。頁面引用越多不同網域(第三方腳本、字型、圖片 CDN、廣告),就要做越多次 DNS。
優化:
減少不同來源(origin)的數量;能放同源就放同源。
對確定會用到、但不是最關鍵的第三方來源使用 。
選擇回應快、節點多的 DNS 供應商;設定合理的 TTL(太短會讓解析器頻繁重查)。
發布 HTTPS DNS 記錄(SVCB/HTTPS RR),可讓支援的瀏覽器在第一次連線就知道伺服器支援 HTTP/3,省去以 標頭發現 h3 的那一輪。
TCP 交握與慢啟動
慢的原因:三向交握固定要 1 RTT。之後 TCP 的機制會限制初始可送出的資料量(初始壅塞視窗 initcwnd 常見為 10 個 MSS,約 14 KB);超過這個大小的回應需要等下一個 RTT 才能繼續送。這也是「關鍵 CSS 內嵌並控制在約 14 KB 內」這類建議的來源。
優化:
讓伺服器離使用者更近:這正是 CDN 的核心價值()。
重用連線:HTTP/1.1 keep-alive、HTTP/2 單一連線多路複用(multiplexing),避免對同一來源開多條連線各付一次交握成本。
對最關鍵的跨來源使用 提前建立連線(見下文)。
TLS 交握
慢的原因:TLS 1.2 需要 2 RTT;憑證鏈太長(多個中繼憑證)會讓 ServerHello 那一封變大而跨越多個封包;客戶端若需自行向 CA 查 OCSP 狀態,又是一次額外連線。
優化:
啟用 TLS 1.3(1 RTT,且是 0-RTT 恢復的前提)。
啟用 OCSP stapling,由伺服器把憑證狀態一併送出,省去客戶端查詢。
縮短憑證鏈、使用 ECDSA 憑證(比 RSA 小且快)。
啟用 session resumption;若使用 0-RTT,注意重放攻擊風險,只允許冪等的 GET 走 early data。
升級到 HTTP/3:QUIC 把傳輸與 TLS 1.3 交握合併成 1 RTT,並解決 TCP 層的隊頭阻塞(head-of-line blocking),在封包遺失率高的行動網路上優勢明顯。
Service Worker 啟動
慢的原因:若導覽被 Service Worker 控制,瀏覽器必須先啟動 worker、執行其腳本,再進入 事件處理。對於必須走網路的 HTML(無法預先快取的動態頁面),這段啟動時間是純粹的額外延遲。
優化:使用 讓瀏覽器在啟動 worker 的同時就先發出網路請求;保持 SW 腳本精簡;避免在啟動階段做重工。細節見。
伺服器處理時間(think time)
嚴格說這已經在 fetch 之後,但它是 TTFB 最常見的元兇:資料庫查詢、未快取的 SSR、冷啟動的無伺服器函式、串接第三方 API。優化手段橫跨(快取 HTML 與 API 回應)、(邊緣快取、103 Early Hints)與(串流 SSR、靜態產生)。
把往返「提前做」:資源提示與 Early Hints
dns-prefetch 與 preconnect
<head>
<!-- 最關鍵的跨來源(例如放 LCP 圖片的 CDN):DNS + TCP + TLS 一次做完 -->
<link rel="preconnect" href="https://img.example-cdn.com">
<!-- 若該來源會以 CORS 模式載入(字型、fetch()),需加 crossorigin,
否則瀏覽器會另開一條匿名連線,preconnect 等於白做 -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 次要的第三方:只先做 DNS -->
<link rel="dns-prefetch" href="https://www.googletagmanager.com">
</head>只對確定會用到、且很快就會用到的來源使用,一般建議 3–5 個以內。每條預先建立的連線都消耗 CPU、記憶體與頻寬(TLS 憑證下載);Chrome 對閒置約 10 秒未使用的預連線會直接關閉,等於浪費。
對於 (預先載入某個特定資源)與 (只建立連線)要分清楚:知道確切 URL 時用前者,只知道來源時用後者。
資源提示只是「提示」;瀏覽器可以忽略。永遠以量測結果為準。
HTTP 103 Early Hints
當伺服器需要幾百毫秒產生 HTML(例如查資料庫、未命中快取的 SSR)時,連線其實是閒置的。103 Early Hints(RFC 8297)允許伺服器在最終回應之前,先送出一個暫時回應,夾帶 標頭告訴瀏覽器「你等一下一定會需要這些東西,先去連線 / 下載」。
HTTP/2 103 Early Hints
Link: </assets/app.css>; rel=preload; as=style
Link: </assets/hero.avif>; rel=preload; as=image; fetchpriority=high
Link: <https://img.example-cdn.com>; rel=preconnect
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
...
要點:
收益上限就是伺服器的 think time:如果 HTML 已從 CDN 邊緣快取以 10 ms 回應,Early Hints 幾乎沒有幫助;若 TTFB 有 300–500 ms 的伺服器處理時間,它能讓 LCP 圖片與 CSS 提早數百毫秒開始下載。
從瀏覽器到發出 103 的那一跳,整條路徑都必須是 HTTP/2 或 HTTP/3;中間任何一段 HTTP/1.1 或會緩衝的反向代理都會讓 103 消失。
目前 Chromium 系瀏覽器與 Firefox 支援,Safari 不處理 103(會忽略,無害)。
主流 CDN(Cloudflare、Fastly、CloudFront 等)多半可在邊緣代為產生 103。
連線建立之後:關鍵渲染路徑速覽
雖然本章主題是「fetch 之前」,但要理解的 FP、FCP、LCP,必須知道第一個 HTML 位元組抵達後瀏覽器做了什麼。這條路徑稱為:
max width=
幾個直接影響 FP / FCP / LCP 的事實:
HTML 是串流解析的:第一個 chunk 到達就開始建立 DOM,不必等整份文件下載完。這是串流 SSR()能提早 FCP 的基礎。
CSS 是 render-blocking:在 中的外部樣式表下載並解析完成之前,瀏覽器不會繪製任何東西(因為任何一條規則都可能改變畫面)。因此 FP / FCP 至少要等到關鍵 CSS 到齊。
同步 是 parser-blocking:解析器遇到沒有 / 的腳本會停下來等它下載並執行(若前面還有未完成的 CSS,還得等 CSS,因為腳本可能讀取樣式)。這會延後後續 DOM 的建立與其中圖片的發現。
Preload scanner 只看得到 HTML 原始碼裡的資源:由 JavaScript 動態插入、或藏在 CSS 裡的圖片,要等到腳本執行或 CSSOM 建好才會被發現。這正是 LCP「資源載入延遲」()的主要來源。
Layout 與 Paint 的成本與 DOM 大小、CSS 複雜度成正比:大量節點、深層巢狀、複雜選擇器會讓每次樣式重算與版面計算變慢,也會拉長互動後的呈現延遲(INP 的 presentation delay)。
課堂實作
在任何網站的 DevTools Console 貼上下面程式碼,觀察各階段的耗時。試著比較「第一次造訪」與「重新整理」的差異(DNS、TCP、TLS 是否變成 0? 是否為 0?)。
const nav = performance.getEntriesByType('navigation')[0];
const ms = (a, b) => Math.round(b - a);
console.table({
'重導 redirect': ms(nav.redirectStart, nav.redirectEnd),
'SW 啟動 worker': nav.workerStart ? ms(nav.workerStart, nav.fetchStart) : 0,
'DNS': ms(nav.domainLookupStart, nav.domainLookupEnd),
'TCP': ms(nav.connectStart, nav.secureConnectionStart || nav.connectEnd),
'TLS': nav.secureConnectionStart ? ms(nav.secureConnectionStart, nav.connectEnd) : 0,
'請求→首位元組': ms(nav.requestStart, nav.responseStart),
'TTFB(自導覽開始)': Math.round(nav.responseStart),
'回應下載': ms(nav.responseStart, nav.responseEnd),
'協定 nextHopProtocol': nav.nextHopProtocol, // h2 / h3 / http/1.1
'重導次數': nav.redirectCount,
'傳輸大小 transferSize': nav.transferSize, // 0 代表來自快取
});開啟 DevTools \(\rightarrow\) Network,勾選「Disable cache」,重新整理頁面。
點選第一個請求(HTML 文件),切到 Timing 分頁,觀察:Queueing、Stalled、DNS Lookup、Initial connection、SSL、Request sent、Waiting for server response(TTFB)、Content Download。
在欄位標題按右鍵開啟 Protocol 欄,看看哪些資源走 h2、哪些走 h3、哪些仍是 http/1.1。
用 Network throttling 切到「Slow 4G」,再看一次各階段的變化:哪些階段被放大得最多?
curl -o /dev/null -s -w \
'DNS %{time_namelookup}s | TCP %{time_connect}s | TLS %{time_appconnect}s | TTFB %{time_starttransfer}s | 總計 %{time_total}s | 重導 %{num_redirects} 次\n' \
https://www.example.com/
# 加上 --http3 可測 HTTP/3(需 curl 支援),加上 -L 會跟隨重導注意 curl 顯示的是累計時間( 包含 DNS 與 TCP),需自行相減。
某網站的行動端使用者 RTT 約 120 ms,伺服器使用 TLS 1.2、HTTP/2,且首頁 會 301 到 。請估算「按下 Enter 到第一個 HTML 位元組」至少要多少毫秒(不含 DNS 與伺服器處理)。若改為 HSTS preload + TLS 1.3 + HTTP/3,可以省下多少?
為什麼對載入字型的來源做 時必須加上 ?如果忘了加會發生什麼事?
一個頁面的 HTML 由 CDN 邊緣快取在 15 ms 內回應,工程師提議加上 103 Early Hints 來加速 LCP 圖片。你會怎麼評估這個提案?
為什麼「把 LCP 圖片改成 CSS 」常常會讓 LCP 變慢?這和 preload scanner 有什麼關係?
一次導覽在真正送出請求前要經過:卸載舊頁 \(\rightarrow\) HSTS / 重導 \(\rightarrow\) Service Worker 啟動 \(\rightarrow\) HTTP 快取檢查 \(\rightarrow\) DNS \(\rightarrow\) TCP \(\rightarrow\) TLS。這些全部是等待時間,直接墊高 TTFB。
RTT 由距離與路由決定,是連線成本的基本單位。新連線成本:TLS 1.2 為 4 RTT、TLS 1.3 為 3 RTT、QUIC 為 2 RTT、0-RTT 恢復可到 1 RTT。
優化手法分三類:省掉往返(移除重導、HSTS、快取、連線重用)、縮短每次往返(CDN、就近節點)、提前做往返(dns-prefetch、preconnect、103 Early Hints、Speculation Rules)。
第一個位元組抵達後進入關鍵渲染路徑:HTML 串流解析、CSS 阻塞渲染、同步腳本阻塞解析、preload scanner 只認得 HTML 原始碼中的資源。