Cache-Control 快取頭怎麼配:按資源型別給出瀏覽器與 CDN 配置表

部落格 / 邊緣與 CDN

Cache-Control 快取頭怎麼配:按資源型別給出瀏覽器與 CDN 配置表

帶 hash 的靜態檔案用 Cache-Control: public, max-age=31536000, immutable;HTML 用 no-cache,讓瀏覽器每次回源確認;使用者私有資料用 private 或 no-store;想讓 CDN 快取而瀏覽器不快取,用 s-maxage 把兩層分開。cache-control 難配,難在一個響應頭要同時管瀏覽器和 CDN 兩層快取。下面先把指令講清,再按資源型別給出配置表,最後用 curl -I 驗證配置有沒有生效。

Cache-Control 管的是哪兩層快取?

一個請求從瀏覽器出發,可能經過兩層快取:

  • 私有快取:瀏覽器自己的磁碟和記憶體快取,只服務當前使用者。
  • 共享快取:CDN 邊緣節點、反向代理(Nginx proxy_cache、Varnish 等),同一份副本會發給許多使用者。

大多數配置錯誤出在把兩層混為一談。例如給登入後的頁面加了 public, max-age=600,瀏覽器快取沒問題,CDN 卻可能把 A 使用者的頁面發給 B 使用者。反過來,給 HTML 加了 no-store,CDN 一次都不能快取,所有流量都打回源站。

所以配快取頭之前,先回答兩個問題:這份響應能不能給別人看?內容變了以後,最多能接受多久的舊資料?

no-cache、no-store、max-age 有什麼區別?

這幾個指令最常被混用,含義如下:

指令含義常見誤解
max-age=N響應在 N 秒內是新鮮的,可以不問源站直接用以為改了檔案使用者就能馬上看到
no-cache可以存,但每次使用前必須回源確認(協商快取)以為是“不快取”
no-store任何快取都不許存以為和 no-cache 一樣
private只允許瀏覽器快取,共享快取不能存以為能保護敏感資料不落盤(不能,瀏覽器仍會存)
public明確允許共享快取存,包括帶 Authorization 的請求以為不寫就不能被 CDN 快取
s-maxage=N只對共享快取生效,覆蓋 max-age以為瀏覽器也會讀
must-revalidate過期後必須回源確認,不能直接用舊副本以為過期前也要確認
immutable新鮮期內內容不會變,重新整理頁面也不必回源確認以為可以單獨使用
stale-while-revalidate=N過期後 N 秒內可以先返回舊副本,同時在後臺更新以為所有瀏覽器和 CDN 都支援
stale-if-error=N源站出錯時,過期 N 秒內的舊副本仍可返回同上,支援情況要查具體 CDN 文件

簡單記:no-cache 是“用之前先問”,no-store 才是“不許存”。對 HTML 用 no-cache 配合 ETag,大多數請求只回一個 304,幾乎不耗頻寬,同時保證使用者拿到最新入口。

ETag 和 Last-Modified 在什麼時候起作用?

no-cache 或 max-age 到期後,快取會帶上條件請求回源:

  • 上次響應有 ETag: "abc123",就發 If-None-Match: "abc123";
  • 上次響應有 Last-Modified,就發 If-Modified-Since。

源站判斷內容沒變,返回 304 Not Modified,不帶響應體。ETag 比 Last-Modified 精確,後者只到秒級,而且多臺源站檔案時間不一致時會誤判。負載均衡後面有多臺機器時,要確認它們生成的 ETag 一致(Nginx 預設按修改時間和檔案大小生成,部署時保留檔案時間即可),否則協商快取會一直失敗。

可以這樣手動驗證:

# 第一次取 ETag
curl -sI https://example.com/ | grep -i etag
# 帶上 ETag 再請求,期望返回 304
curl -sI -H 'If-None-Match: "上一步拿到的值"' https://example.com/ | head -1

不同資源該怎麼配?一張表直接照抄

下面是常見的起點配置,按自己的釋出頻率調整數字:

資源型別推薦 Cache-Control說明
帶 hash 的 JS/CSS/字型(app.3f9a1c.js)public, max-age=31536000, immutable檔名隨內容變化,舊檔案永遠不會被改寫
HTML 入口頁no-cache配合 ETag,每次確認;釋出後使用者馬上拿到新的 hash 引用
HTML,想讓 CDN 也扛流量public, max-age=0, s-maxage=60, stale-while-revalidate=30瀏覽器每次回源,CDN 快取 60 秒;釋出時要清 CDN 快取
不帶 hash 的圖片(/logo.png)public, max-age=86400改圖時換檔名比等過期可靠
使用者上傳的圖片(URL 唯一)public, max-age=2592000URL 不復用就可以放長
公共 API(排行榜、狀態資料)public, max-age=0, s-maxage=30, stale-while-revalidate=60CDN 吸收突發流量,瀏覽器總拿較新資料
登入後的 API、個人頁面private, no-cache瀏覽器可快取但要確認,CDN 不許存
支付、令牌、敏感個人資訊no-store哪一層都不留副本
robots.txt、sitemap.xmlpublic, max-age=3600改動不頻繁,一小時足夠

有兩條容易漏:

  1. 不帶 hash 的檔案不要用 immutable。瀏覽器會在一年內都不再確認,改了檔案也看不到。
  2. 響應裡有 Set-Cookie 時要小心。很多 CDN 預設不快取帶 Set-Cookie 的響應,但不是全部;如果給這類響應配了 public, s-maxage,可能把會話 Cookie 快取下來發給其他人。公共頁面應在源站去掉不必要的 Set-Cookie。

s-maxage 和 CDN-Cache-Control 怎麼分開管兩層?

只用一個 Cache-Control 時,靠 s-maxage 區分:瀏覽器讀 max-age,共享快取優先讀 s-maxage。

Cache-Control: public, max-age=60, s-maxage=3600

意思是瀏覽器快取 1 分鐘,CDN 快取 1 小時。這樣釋出新版本後,清一次 CDN 快取,使用者最多等 1 分鐘就能看到。

問題是 s-maxage 對源站和 CDN 之間所有共享快取都生效,包括你沒法控制的中間代理。RFC 9213 定義了 CDN-Cache-Control 頭,只給 CDN 讀,瀏覽器會忽略它:

Cache-Control: public, max-age=60
CDN-Cache-Control: max-age=3600

Cloudflare 還支援只給自己讀、不會轉發給下游 CDN 的 Cloudflare-CDN-Cache-Control。不同 CDN 對這些頭的支援程度不一樣,上線前查一下自己所用 CDN 的官方文件,再用下面的方法實測。

怎麼用 curl 判斷 CDN 有沒有命中?

最直接的方法是連續請求兩次,看響應頭:

curl -sI https://example.com/static/app.3f9a1c.js | grep -iE 'cache-control|age|cf-cache-status|x-cache|etag|last-modified|vary'

關注這幾個頭:

響應頭怎麼看
Age副本在共享快取裡已經存了多少秒。第二次請求時出現且數值在增長,說明命中了快取
cf-cache-status(Cloudflare)HIT 命中;MISS 未命中已回源;EXPIRED 過期後回源;REVALIDATED 協商後複用;UPDATING/STALE 返回了舊副本;BYPASS 被規則或頭部繞過;DYNAMIC 該資源不在快取範圍內
X-Cache 或類似頭其他 CDN 常用的命中標記,具體取值看該 CDN 文件
Vary快取鍵會包含這些請求頭。Vary: * 或 Vary: Cookie 會讓命中率大幅下降

一個排查順序:

  1. 先看源站本身返回的頭,繞過 CDN 直接請求源站(curl -sI --resolve example.com:443:源站IP https://example.com/路徑),確認 Cache-Control 寫對了。
  2. 再經 CDN 請求兩到三次,看 cf-cache-status 是否從 MISS 變成 HIT,Age 是否出現。
  3. 一直是 MISS:檢查是否有 Set-Cookie、private、no-store,或者查詢引數每次都不一樣(比如帶時間戳)。
  4. 一直是 DYNAMIC:Cloudflare 預設只快取按副檔名識別的靜態檔案,HTML 和 API 需要單獨寫快取規則。
  5. 使用者反饋看不到更新:看 Age 和 max-age,判斷是瀏覽器快取還是 CDN 快取沒過期。

注意,同一個 URL 在不同地區由不同邊緣節點服務,各自有獨立的快取。你在一個地方測到 HIT,另一個地區的使用者第一次訪問仍然可能是 MISS。測量 CDN 速度時也要考慮這一點:cdnping 的探測怎樣避開快取對結果的影響,寫在資料方法頁面;公共前端 CDN 的速度對比可以看 CDN 速度榜。

Nginx 怎麼配?

一個按資源型別分開的示例:

# 帶 hash 的靜態資源
location ~* "\.[0-9a-f]{8,}\.(js|css|woff2)$" {
    add_header Cache-Control "public, max-age=31536000, immutable" always;
}

# 普通圖片
location ~* \.(png|jpg|jpeg|gif|webp|avif|svg)$ {
    add_header Cache-Control "public, max-age=86400" always;
}

# HTML 入口
location / {
    try_files $uri $uri/ /index.html;
    add_header Cache-Control "no-cache" always;
}

# 登入後的 API
location /api/me {
    proxy_pass http://backend;
    proxy_hide_header Cache-Control;
    add_header Cache-Control "private, no-cache" always;
}

幾個 Nginx 的坑:

  • 正則裡含 { } 時要用引號把整個正則括起來,否則 Nginx 會把花括號當成塊的開始,nginx -t 直接報錯。
  • add_header 預設只對 200、204、206、301、302、304 等狀態碼生效,加 always 才會對 4xx/5xx 也生效。錯誤頁一旦被 CDN 按長快取存下,修好源站後使用者仍會看到錯誤。
  • 子 location 裡只要寫了一條 add_header,就不會繼承上級的任何 add_header。安全頭寫在 server 塊裡時,經常因此在某個 location 裡丟失。
  • expires 30d; 會同時生成 Expires 和 Cache-Control: max-age,和手寫的 add_header Cache-Control 疊加時可能出現兩條頭,二選一即可。
  • 後端已經返回 Cache-Control 時,再 add_header 會出現兩條,用 proxy_hide_header 先去掉後端的那條。

Cloudflare 上要注意什麼?

Cloudflare 預設遵守源站的 Cache-Control(包括 s-maxage、private、no-store),但預設只快取按副檔名識別的靜態資源。要快取 HTML 或 API,需要在 Cache Rules 裡給匹配的路徑設定“符合快取條件”,然後在規則裡選擇:

  • 邊緣 TTL:“使用源站快取控制頭”時按你的 s-maxage/max-age 走;也可以在規則裡覆蓋。
  • 瀏覽器 TTL:可以單獨覆蓋瀏覽器端的 max-age,但不要把它設得比源站更長,否則釋出後使用者會卡在舊版本。
  • 快取鍵:去掉無意義的查詢引數(如 utm_*),可以明顯提高命中率。

帶 Authorization 頭或會話 Cookie 的路徑,不要寫進“符合快取條件”的規則裡,除非你確定響應對所有人相同。AI 應用的場景(流式響應、API 限速、回源保護)在用 Cloudflare 加速 AI 應用裡有更細的配置說明。

上線前的檢查表

  • 所有構建產物都帶內容 hash,且只有它們用了 immutable
  • HTML 入口是 no-cache,或者 CDN 層快取時間很短且釋出流程裡有清快取這一步
  • 登入後介面全部是 private 或 no-store,用未登入的 curl 請求一次,確認拿不到別人的資料
  • 公共頁面不返回 Set-Cookie
  • 錯誤響應(404、500)的快取時間有明確設定,沒有被快取成長期
  • Vary 只包含必要的頭,通常是 Accept-Encoding
  • 用 curl -I 連續請求兩次,看到 Age 或 HIT
  • 繞過 CDN 直連源站,響應頭和預期一致

常見問題

改了 CSS,使用者還是看到舊樣式?

多半是檔名沒帶 hash,又配了較長的 max-age。根治辦法是讓構建工具輸出帶 hash 的檔名,HTML 改為 no-cache。臨時辦法是清 CDN 快取,但瀏覽器裡已有的副本只能等過期。

Cache-Control: no-cache 會不會讓網站變慢?

對 HTML 來說,多出來的是一次條件請求,內容沒變時返回 304,只有響應頭沒有響應體。真正影響速度的通常是連線建立本身,DNS、TCP、TLS 各佔多少可以參考AI API 延遲拆解裡的拆法,同樣適用於網頁請求。

npm 映象、軟體包下載也受這些頭影響嗎?

受影響。映象站和 registry 對包後設資料和 tarball 一般採用不同的快取策略:後設資料會變,快取時間短;tarball 版本固定,可以長期快取。裝包慢時可以對照npm 映象源怎麼設定裡的排查思路。

這裡說的快取和 Claude 的提示快取是一回事嗎?

不是。HTTP 快取存的是完整響應,由響應頭控制;Claude API 的提示快取(prompt caching)是服務端複用提示字首的計算結果,用請求引數控制,細節見Claude API 提示快取怎麼用。

相關閱讀

如果網站面向中國大陸使用者,CDN 選型和節點覆蓋同樣影響體驗,可以先讀國內怎麼選公共 CDN;整體網路排查順序見開發者網路自檢清單。需要基於 Cloudflare 全球網路做邊緣加速時,可以看 cdnping Edge 的說明。

相關文章

繼續閱讀