Web 服务

HTTP 缓存实战:Cache-Control 与 ETag 的正确配法

缓存配错只有两种结局:用户看不到新版,或回源流量打爆源站。本文讲清强缓存与协商缓存的分工、指纹资源与 HTML 的策略分治,并给出可直接用的 Nginx 配置与验证命令。

作者:巧匠团队·8 分钟阅读·更新于 2026-08-31

两类缓存,一套指令

HTTP 缓存分两类:浏览器私有缓存与 CDN/Nginx 共享缓存,Cache-Control 的指令同时约束两者。配错方向只有两种结局:要么用户更新看不到,要么回源流量打爆源站。

先明确目标再动手:带内容指纹的静态资源追求「长强缓存」,HTML 与接口追求「每次校验新鲜度」——两种策略截然不同,必须分开配置。

# 响应头速查(下文逐个展开)
# Cache-Control: public, max-age=31536000, immutable   静态资源
# Cache-Control: no-cache                              HTML/接口(可缓存但需校验)
# Cache-Control: no-store                              敏感数据,禁止缓存

强缓存:Cache-Control 指令逐个讲清

`max-age=N` 秒数内直接用本地缓存、不发请求;`s-maxage` 只约束共享缓存且优先级更高;`public`/`private` 决定能否被 CDN 存;`immutable` 告诉浏览器资源永不变(指纹 URL 专用,连刷新都不校验)。

最容易误解的两个:`no-cache` 不是「不缓存」,而是「缓存前必须协商校验」;`no-store` 才是彻底不落缓存。Expires 是 HTTP/1.0 的绝对时间遗物,被 Cache-Control 覆盖,现代配置只写后者。

Cache-Control: public, max-age=31536000, immutable
# 指纹资源:一年强缓存 + 永不校验

Cache-Control: private, max-age=0, must-revalidate
# 私有内容:每次都校验

协商缓存:ETag 优先于 Last-Modified

强缓存过期后进入协商:浏览器带 `If-None-Match: <ETag>`(或 `If-Modified-Since`)问服务器「变了吗」,没变回 304(无响应体,几乎零流量),变了回 200 + 新内容。

优先 ETag:它是内容级标识,而 Last-Modified 只精确到秒、且修改时间变了内容未必变。Nginx 默认给静态文件自动生成 ETag(基于 mtime+size),动态内容则需要应用自己算(内容 hash 是稳妥做法)。

# 手动模拟一次协商
curl -sI https://laohand.com/index.html | grep -i etag
curl -sI -H "If-None-Match: <上一步拿到的值>" https://laohand.com/index.html | head -1
# 期望输出: HTTP/1.1 304 Not Modified

策略分治:指纹资源与 HTML 各走各路

构建产物带内容 hash(app.a1b2c3.js)→ 内容一变文件名就变,每个 URL 永远只对应一份内容,放心给 `max-age=31536000, immutable`;而 HTML 引用新 hash 的资源,所以 HTML 本身必须 `no-cache`(每次协商),用户才能及时看到新版本。

没指纹的静态资源用短 `max-age`(如 5 分钟)+ 协商兜底;接口响应默认 `private, no-cache`,只有明确可公开缓存的列表类数据才给 `public`。这套分治是「用户秒开」与「发版即生效」兼得的唯一解。

# 典型分治
/app/assets/*   → public, max-age=31536000, immutable
/index.html     → no-cache
/api/*          → private, no-cache

Nginx 落地配置

按 location 分流:指纹化静态目录给一年长缓存;HTML 显式 `no-cache`(注意 `expires -1` 输出的是 no-cache 语义);代理后端的响应头按需透传或覆盖。Nginx 静态文件的 ETag 默认开启,无需额外配置。

CDN 场景给共享缓存加 `s-maxage`,源站同时保留更短的 `max-age` 约束浏览器——两层缓存各自的过期时间互不干扰。

location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
location = /index.html {
    add_header Cache-Control "no-cache";
}
# 静态文件 ETag 默认开启,无需额外配置

验证与高频排错

验证链路:`curl -I` 看响应头是否符合预期;DevTools Network 的 Size 列显示 (memory cache)/(disk cache) 即强缓存命中,显示 304 即协商命中。

两个高频坑:① DevTools 打开时 Network 面板默认勾选 Disable cache,测出的结果不代表真实用户;② CDN 层另有一套缓存,改了源站响应头还要刷新 CDN 才生效。「改了代码用户看不到新版」九成是 HTML 被强缓存——把 HTML 调成 no-cache 即可根治。

curl -sI https://example.com/app.a1b2.js | grep -i cache-control
# DevTools: Network → Size 列
#   (memory/disk cache) = 强缓存命中
#   304                  = 协商命中