主題: Web platform

網站更新了,為什麼還是舊畫面?從 HTTP 快取到新版 JS 的路線

用瀏覽器與 CDN 的例子理解 max-age、s-maxage、Age、條件式請求與 Vary,再把 HTML 更新和內容指紋資源接起來。

來源端已有新版,不代表使用者收到新版;先確認這份回應究竟來自瀏覽器快取、CDN,還是 origin。 · 來源:GIPHY

新版已部署,畫面卻沒變。開啟開發者工具、勾選 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。整個過程不需要換網域。

新檔案即使已上線,HTML 仍必須指向它的 URL;瀏覽器不會自行搜尋最新版本。 · 來源:GIPHY

內容指紋資源適合長快取,是因為內容改了就使用新 URL。對保持內容不變的公開資源,可以評估:

Cache-Control: public, max-age=31536000, immutable

這個例子不是全站通用設定。每天覆寫的固定 URL 圖片,或經常更新的 HTML,都不能直接套用相同前提。

部署還要照顧已開啟的舊頁面。它稍後可能要求載入舊 chunk;立刻刪除所有舊版檔案,可能讓請求得到 404。新 HTML、新資源的可用順序,以及舊檔保留時間,需要一起安排。

Jake Archibald 的快取文章 用具體發布情境討論這些取捨。它是 2016 年的經典文章,這裡採用的是版本化與更新配套的觀念,而非舊工具建議。

下次遇到舊畫面,可以怎麼查?

  1. 在 Network 確認 HTML、CSS、JS 的實際 URL。先看 HTML 是否引用預期版本。
  2. 檢查 Cache-Control、Age、ETag、Last-Modified 與 Vary,搭配瀏覽器顯示的快取來源。只看 200 不能判定一定回到來源伺服器。
  3. 分別測試全新瀏覽器與先載入舊版的瀏覽器,再部署新版。不要只在 Disable cache 開著時驗證。
  4. 若清除了 CDN,仍要考慮訪客瀏覽器已有的 fresh 回應。兩層快取不會一起被清除。

可重跑的更新示範:200 → 304 → 200

下載 HTTP 快取示範,以 Node.js 24 或更新版本執行:

node http-cache-demo.mjs

程式只在 127.0.0.1 的隨機埠啟動暫存伺服器,自動發出請求、斷言結果後關閉。2026-09-10 以 Node.js 26.8.1 執行,得到以下順序;v1/v2 是示範版本,ETag 與資源檔名由內容雜湊產生。

步驟 請求 回應與檢查
首次取得 GET / 200;HTML 引用 v1 資源,Cache-Control 是 no-cache
版本未變 GET /,If-None-Match 帶舊 ETag 304,本文為空
切換成 v2 後重驗 GET /,仍帶舊 ETag 200,新 ETag 與新資源網址
已開啟的舊頁繼續載入 GET 舊資源網址 200,仍是 v1 JS
新頁載入 GET 新資源網址 200,是 v2 JS;長快取只用於內容指紋資源
不存在的資源 GET /missing.js 404,不用 HTML 偽裝成功

Node 的 fetch 不會在這裡替我們模擬瀏覽器快取;條件標頭由測試明確送出。伺服器也只接受示範用的單一精確 ETag,沒有實作完整 HTTP validator parser。這份檔案用來看懂契約,不適合作為 production 快取伺服器。

實際排查網站時,先保留一般快取設定載入舊版,再發布新版並重新導覽。核對 HTML 的資源網址、ETag、Age 與來源,接著開新瀏覽器情境比較。若只有勾選 Disable cache 才正常,問題仍未解決;若用了 Service Worker,還要另外檢查它的攔截與儲存。

本次可執行示範驗證 HTTP 回應與資源保留,沒有測 CDN 清除,也沒有證明本站正式部署已採用這套標頭。ETag 官方說明可用來核對條件式請求的語意。

我學到什麼

  • 我會把能否保存、能否直接重用,以及如何驗證分開判斷。
  • 我會先查回應來自哪一層,再解讀 max-age、s-maxage 與 Age。
  • 我會把驗證標頭與 Vary 分清楚,前者確認內容,後者區分可匹配的版本。
  • 我會把 HTML 引用更新與資源保留視為部署的一部分,不能只確認新 JS 已上傳。

外部參考資料