主題: Web platform
網站更新了,為什麼還是舊畫面?從 HTTP 快取到新版 JS 的路線
用瀏覽器與 CDN 的例子理解 max-age、s-maxage、Age、條件式請求與 Vary,再把 HTML 更新和內容指紋資源接起來。
動態迷因(展開/收合)
新版已部署,畫面卻沒變。開啟開發者工具、勾選 Disable cache,一切又正常了。這個測試能提供線索,卻還不能證明一般訪客也拿得到新版。
我會先查正在使用的 URL 和回應標頭。快取可能完全照設定運作,只是那份設定容許舊內容繼續使用。
本文談一般 HTTP 快取。Service Worker 自行管理的資料、框架的資料快取與上一頁快照,需要另外查各自的規則。
先分清楚是誰在保存
瀏覽器的 HTTP 快取服務單一使用者;CDN 等共享快取可以把同一份公開回應提供給不同訪客。頁面裡用 JavaScript Map 保存的查詢結果,則由程式自行管理,不會因為名字也叫快取,就自動遵守 HTTP 政策。
來源伺服器改了檔案,已保存回應的快取不會因此自動收到通知。只要舊回應仍 fresh,而且政策允許,它就可能繼續被使用。stale 也不表示檔案一定變了,只代表新鮮期限已過。
這些分類與流程可對照 MDN HTTP caching。排查時,先問哪一層提供回應,再問它為什麼認為可以使用。
max-age 與 s-maxage,各自管哪裡?
假設所有人看到相同的公開商品介紹:
Cache-Control: max-age=120, s-maxage=900
Age: 180
| 保存位置 | 使用的期限 | age 為 180 秒時 |
|---|---|---|
| 瀏覽器 | max-age 的 120 秒 | 已 stale |
| CDN 等共享快取 | s-maxage 的 900 秒 | 仍 fresh |
兩個數字的單位都是秒。瀏覽器忽略 s-maxage;共享快取若看到它,會優先採用它,而不是 max-age。這讓瀏覽器與 CDN 可以採不同的期限。
Age 也要一起看。上例的回應已有 180 秒年齡,送進瀏覽器後,不會重新得到 120 秒的新鮮時間。實際年齡計算還涉及 Date 與傳輸時間;此例直接給定年齡,重點是它不會每經過一層就歸零。
這種配置能讓 CDN 分擔公開內容的流量,但瀏覽器期限短,不保證來源伺服器的修改立即可見。CDN 仍可能提供它認為 fresh 的版本。需要立即更新時,必須把 CDN 清除或版本化策略一起規劃。MDN Cache-Control 說明各指令與 Age 的關係。
也別把期限當成保存保證。瀏覽器可以因空間不足或使用者清除資料而移除快取。
no-cache 可以存,no-store 才是不要存
no-cache 的名稱很容易讓人誤會。它允許保存回應,但一般 HTTP 重用前必須成功驗證。no-store 才是要求 HTTP 快取不要儲存該回應。
會修正的文章 HTML,可以從這種策略開始評估:
Cache-Control: no-cache
ETag: "article-v3"
個人化回應若允許瀏覽器保存、卻不允許共享快取保存,可以使用 private。若需求是不保存,應考慮 no-store。有登入 Cookie 或 HTTPS,都不能取代正確的快取政策。這些設定也不是抹除先前所有副本的遠端刪除指令。
驗證有兩組配對,Vary 不在這裡
瀏覽器已保存 article-v3,要詢問是否仍適用,可以送出:
GET /guide HTTP/1.1
If-None-Match: "article-v3"
正常成功處理這個條件式 GET 時,版本相符便可回 304,讓瀏覽器重用既有本文;版本改了則回 200 與新內容。304 省下本文傳輸,仍然有網路往返。
另一種驗證依據是修改時間:
Last-Modified: Tue, 08 Sep 2026 02:00:00 GMT
後續請求帶的是:
If-Modified-Since: Tue, 08 Sep 2026 02:00:00 GMT
| 回應提供的依據 | 驗證請求使用的標頭 |
|---|---|
| ETag | If-None-Match |
| Last-Modified | If-Modified-Since |
不要把 Last-Modified 放進 Vary 來詢問更新。它們處理不同問題。另外,有 ETag 不代表每次都必須驗證;仍 fresh 且政策允許直接重用時,不必只因 ETag 存在就發請求。
細節見 If-None-Match 與 If-Modified-Since。
Vary 是分版本,不是設定壓縮方法
同一個 URL 可能依 Accept-Encoding 回傳不同壓縮版本。例如請求接受 gzip,回應可以包含:
Content-Encoding: gzip
Vary: Accept-Encoding
Content-Encoding 描述這份內容使用的編碼;Vary 告訴快取,匹配已存回應時要考慮哪些請求標頭。Cache-Control 則處理保存與重用政策,沒有 Cache-Control: gzip 這種標準指令。
同理,伺服器若依 Accept-Language 選擇語言,可用 Vary: Accept-Language 告知快取。但它不會替文章翻譯。若 /en/help 與 /zh-TW/help 已用不同 URL 區分,而且不依語言標頭改變內容,就不必僅為這個目的再加語言 Vary。MDN Vary 有對應說明。
新 JS 上線了,HTML 有指向它嗎?
瀏覽器拿到的 HTML 如果仍然是:
<script src="/main.aaa.js"></script>
新增 /main.bbb.js 不會讓瀏覽器自行發現它。HTML 需要改成引用新 URL,瀏覽器也需要取得那份新 HTML。整個過程不需要換網域。
動態迷因(展開/收合)
內容指紋資源適合長快取,是因為內容改了就使用新 URL。對保持內容不變的公開資源,可以評估:
Cache-Control: public, max-age=31536000, immutable
這個例子不是全站通用設定。每天覆寫的固定 URL 圖片,或經常更新的 HTML,都不能直接套用相同前提。
部署還要照顧已開啟的舊頁面。它稍後可能要求載入舊 chunk;立刻刪除所有舊版檔案,可能讓請求得到 404。新 HTML、新資源的可用順序,以及舊檔保留時間,需要一起安排。
Jake Archibald 的快取文章 用具體發布情境討論這些取捨。它是 2016 年的經典文章,這裡採用的是版本化與更新配套的觀念,而非舊工具建議。
下次遇到舊畫面,可以怎麼查?
- 在 Network 確認 HTML、CSS、JS 的實際 URL。先看 HTML 是否引用預期版本。
- 檢查 Cache-Control、Age、ETag、Last-Modified 與 Vary,搭配瀏覽器顯示的快取來源。只看 200 不能判定一定回到來源伺服器。
- 分別測試全新瀏覽器與先載入舊版的瀏覽器,再部署新版。不要只在 Disable cache 開著時驗證。
- 若清除了 CDN,仍要考慮訪客瀏覽器已有的 fresh 回應。兩層快取不會一起被清除。
我學到什麼
- 我會把能否保存、能否直接重用,以及如何驗證分開判斷。
- 我會先查回應來自哪一層,再解讀 max-age、s-maxage 與 Age。
- 我會把驗證標頭與 Vary 分清楚,前者確認內容,後者區分可匹配的版本。
- 我會把 HTML 引用更新與資源保留視為部署的一部分,不能只確認新 JS 已上傳。