HTTP 快取 — 同樣的東西不搬運兩次
用 Cache-Control: max-age 手算 fresh 與 stale,變成 stale 之後用 ETag 與 If-None-Match 的條件式 GET 確認,若是 304 就不搬運本文 — 透過附模擬器的方式掌握快取判定的流程。
更新了卻沒變・沉重的頁面一瞬間打開 — 兩者背後都是快取
修好了 CSS 也上傳了,用瀏覽器看卻還是舊的。另一方面,第一次很慢的頁面,第二次卻一瞬間就開好。無論好壞,藏在背後的都是同一個機制 — 快取。它是「同樣的東西不搬運兩次」這個 Web 速度的心臟,但不了解機制的話,也是「不知為何修不好」的頭號原因。
幸好,HTTP 的快取只要會讀標頭就不可怕。判定中登場的問題只有 2 個。
Cache-Control: max-ageETagmax-age 的手算 — fresh 或 stale 由減法決定
伺服器會在回應附上這樣一句:Cache-Control: max-age=1800。意思是「從收到起 1800 秒內可視為新鮮(fresh)」。判定只靠減法。
具體地跑一遍。假設在 10:00:00 收到 max-age=1800(= 30 分鐘)的回應,10:20:00 時的經過是 1200 秒。1200 < 1800,所以還是 fresh,剩餘 600 秒。過了 10:30:00 那一瞬間就變成 stale。常見值的換算也讓手熟悉起來 — max-age=86400 是 1 天(86400 = 24 × 3600),max-age=604800 是 7 天,max-age=31536000 是 365 天。
再畫一條重要的界線:stale ≠ 禁止使用。stale 是「不經確認就不能用」,確認後若沒有變更就能繼續使用。這個「確認」的機制,就是下一位主角 — 條件式 GET。
快取判定的整體流程。左邊綠色的出口(fresh:零通訊)、中央藍色的出口(304:只有標頭)、右邊橘色的出口(200:全部傳輸)— 請用下方的模擬器在這 3 個出口之間來回走走。
練習 5-1 — 手算 max-age
fresh 與 stale 的判定只有加法和減法。請把接收時刻・經過秒數・max-age 寫在紙上計算。
Q1. 回應帶著 Cache-Control: max-age=86400。這份快取能保持 fresh 的期間是幾小時?
max-age 的單位是秒。86400 ÷ 3600 = 24 小時,也就是整整 1 天。86400 秒 = 1 天是快取設定的常見值,和 604800 秒 = 7 天一起讓手記住,設定檔就能讀得順暢。
Q2. 在 10:00:00 收到了 Cache-Control: max-age=1800 的回應。在 10:20:00 的時間點,這份快取能保持 fresh 的剩餘時間是幾秒?
從接收起算的經過時間是 10:00:00 → 10:20:00,20 分鐘 = 1200 秒。與 max-age=1800 秒的差是 1800 − 1200 = 600 秒。也就是從 10:30:00 那一瞬間開始變成 stale。「只是比較經過秒數與 max-age」— fresh 判定的計算就這麼多。
Q3. 快取變成「stale(不新鮮)」時,正確的理解是哪一個?
stale 不是「過了賞味期限就丟」而是「需要確認」的狀態。快取本身還留在手邊,向伺服器附條件地詢問,若得知「沒有變更」(304)就能繼續使用。既不是立刻刪除,也不是可以不經確認繼續使用 — 這個「確認後再用」的機制,就是接下來要讀的條件式 GET。
ETag 與條件式 GET — 「只有變了的時候才給我」
對變成 stale 的快取的確認,是以這樣的兩段式進行的。首先在最初的回應中,伺服器附上本文的「指紋」。
HTTP/1.1 200 OK
Cache-Control: max-age=1800
ETag: "abc123"
Content-Length: 102400
ETag 是代表該時間點本文版本的短識別子。期限過後,瀏覽器送出的不是普通的 GET,而是條件式 GET。
GET /style.css HTTP/1.1
Host: example.com
If-None-Match: "abc123"
If-None-Match 是「我手邊的版本是 abc123。只有與它不同時才給我本文」的條件附註。伺服器的回答分成 2 種。
請在這裡建立傳輸量的手感。fresh 連請求都不發出(0 位元組)。304 會發出請求但只回標頭(數百位元組)。200 是全部(整個本文)。所謂快取設計,就是設計如何把流量分配到這 3 個出口。
用模擬器確認
下方的模擬器重現對 1 個檔案(假設本文 120 KB)的快取判定。請移動 max-age 與經過時間的滑桿、切換 ETag 的有無與伺服器端有無更新的開關,確認請求會不會發出・發出的話是不是條件式・回應是 200 還是 304・傳輸量是多少如何切換。
模擬器的動作概要(文字版):移動 max-age 滑桿與經過時間滑桿時,若經過時間小於 max-age,就判定為「fresh — 不送請求・傳輸量 0 KB」。經過時間在 max-age 以上(stale)時,ETag 開關為關則顯示「普通的 GET → 200 → 約 120.3 KB 傳輸」;ETag 開關為開則變成「帶 If-None-Match 的條件式 GET」,「伺服器上有更新」開關為關時顯示「304 → 約 0.3 KB 傳輸」,為開時顯示「200 → 約 120.3 KB 傳輸」。判定的理由也會逐行以文字顯示。
練習 5-2 — 條件式 GET 與禁止儲存
ETag・If-None-Match・304 的連動,以及 no-store / no-cache 的區分。
Q4. 對於變成 stale 的快取,瀏覽器送出了附上 If-None-Match: W/"abc123" 的 GET。伺服器上的資源沒有變更時,伺服器的正確回應是哪一個?
If-None-Match 是「我手邊快取的 ETag 是 abc123。只有與它不同時才給我本文」的條件式請託。若一致(沒有變更),伺服器就回傳不帶本文的 304 Not Modified,瀏覽器繼續使用手邊的快取。用 200 回傳全文是條件不一致(已更新)時的情況,404 則是資源不存在的另一回事。
Q5. 304 Not Modified 能節省傳輸量的正確理由是哪一個?
304 的意思是「和你手邊的東西相同」— 不需要把同樣的東西再搬一次,所以回應只有標頭(頂多數百位元組)。若本文是 100 KB,傳輸量大約變成數百分之一。但請注意請求本身每次都有發出。連請求都不發出的只有 fresh 期間,304 是「會發出,但很輕」。
Q6. Cache-Control: no-store 與 Cache-Control: no-cache 的正確差異是哪一個?
名字容易混淆的代表。no-store 是「一開始就不准儲存」(適合餘額等敏感資訊),no-cache 是「可以儲存,但每次使用前要確認」(回 304 的話就能用手邊的副本)。較嚴格的是 no-store。「no-cache = 不快取」這種直覺是事故之源,請在這裡把區分固定下來。
no-store 與 no-cache — 不被名字騙了
最後,把因名字容易混淆而出名的 2 個指示區分清楚。兩者都寫在 Cache-Control,但意思的強度完全不同。
與直覺相反,相當於「不要快取」的是 no-store 而不是 no-cache。no-cache 是「使用快取前務必確認」,與 304 搭配的話傳輸量仍會被節省。這個誤解在實務中真的很常見 — 「明明加了 no-cache,裝置上卻還留著副本」正是符合規格的行為。
本章帶回家的重點
- 快取判定的問題有 2 個 — 還 fresh 嗎(與 max-age 的減法),stale 的話內容變了嗎(條件式 GET)
- fresh 期間連請求都不發出。stale 不是「禁止使用」而是「需要確認」
- ETag + If-None-Match → 沒有變更就是 304(不帶本文・只有標頭)。出口有 3 個:0 位元組 / 數百位元組 / 全部傳輸
- no-store 是禁止儲存,no-cache 是每次重新驗證。「不要快取」指的是 no-store
到這裡,3 點組合的讀法已經全部到齊。下一章終於是實物 — 把這套讀法原封不動地套用到 curl 與開發者工具的輸出上。