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=2592000 | URL 不復用就可以放長 |
| 公共 API(排行榜、狀態資料) | public, max-age=0, s-maxage=30, stale-while-revalidate=60 | CDN 吸收突發流量,瀏覽器總拿較新資料 |
| 登入後的 API、個人頁面 | private, no-cache | 瀏覽器可快取但要確認,CDN 不許存 |
| 支付、令牌、敏感個人資訊 | no-store | 哪一層都不留副本 |
robots.txt、sitemap.xml | public, max-age=3600 | 改動不頻繁,一小時足夠 |
有兩條容易漏:
- 不帶 hash 的檔案不要用
immutable。瀏覽器會在一年內都不再確認,改了檔案也看不到。 - 響應裡有
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 會讓命中率大幅下降 |
一個排查順序:
- 先看源站本身返回的頭,繞過 CDN 直接請求源站(
curl -sI --resolve example.com:443:源站IP https://example.com/路徑),確認Cache-Control寫對了。 - 再經 CDN 請求兩到三次,看
cf-cache-status是否從MISS變成HIT,Age是否出現。 - 一直是
MISS:檢查是否有Set-Cookie、private、no-store,或者查詢引數每次都不一樣(比如帶時間戳)。 - 一直是
DYNAMIC:Cloudflare 預設只快取按副檔名識別的靜態檔案,HTML 和 API 需要單獨寫快取規則。 - 使用者反饋看不到更新:看
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 的說明。