HTTP缓存体系-强缓存-协商缓存
1. 两类缓存
| 类型 | 含义 | 是否发请求 |
|---|---|---|
| 强缓存 | 直接用本地,不发请求 | 否(Memory / Disk Cache) |
| 协商缓存 | 发请求验证,304 复用 | 是(带条件头) |
2. 强缓存
2.1 Cache-Control(HTTP/1.1,主流)
Cache-Control: public, max-age=31536000, immutable
| 指令 | 含义 |
|---|---|
public | 任何缓存(浏览器、CDN)可存 |
private | 仅终端浏览器,CDN 不缓存 |
no-cache | 必须协商验证(不是不缓存) |
no-store | 完全不缓存 |
max-age=N | 缓存 N 秒 |
s-maxage=N | CDN 缓存 N 秒(覆盖 max-age) |
immutable | 资源不会变,浏览器不发 if-modified |
stale-while-revalidate=N | 过期 N 秒内用旧的,后台更新 |
must-revalidate | 过期必须验证 |
2.2 Expires(HTTP/1.0,老)
Expires: Thu, 18 Jun 2027 10:00:00 GMT
绝对时间,依赖客户端时钟。Cache-Control 优先级更高。
3. 协商缓存
强缓存过期后发请求带条件头,服务端可以返回 304 Not Modified(不带 body)。
3.1 ETag(推荐)
# 第一次响应
ETag: "abc123"
Cache-Control: no-cache
# 第二次请求
If-None-Match: "abc123"
# 服务端比对:相同则 304,不同则 200 + 新内容
ETag 是文件指纹(hash),精度高。
3.2 Last-Modified
# 响应
Last-Modified: Wed, 17 Jun 2026 10:00:00 GMT
# 请求
If-Modified-Since: Wed, 17 Jun 2026 10:00:00 GMT
时间戳,精度只到秒,文件 1 秒内多次改不准。
ETag 优先级 > Last-Modified。Nginx 默认两者都开启。
4. 决策树
资源是 hash 文件名(app.abc123.js)?
├── 是 → Cache-Control: public, max-age=31536000, immutable
└── 否
├── HTML(入口) → Cache-Control: no-cache
├── 用户私有数据 → Cache-Control: private, no-store
├── 公开 API → Cache-Control: public, max-age=60, s-maxage=600
└── 一般静态 → Cache-Control: public, max-age=3600
5. 实战配置
5.1 Nginx
# JS / CSS / 字体 / 图片 (hash 文件名)
location ~* \.(js|css|woff2?|png|jpg|svg|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML
location ~ \.html$ {
add_header Cache-Control "no-cache";
}
# API
location /api/ {
add_header Cache-Control "no-store";
proxy_pass http://backend;
}
5.2 Vercel / Netlify
vercel.json:
{
"headers": [
{
"source": "/(.*\\.(?:js|css|woff2|png|jpg|svg))",
"headers": [
{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
]
},
{
"source": "/(.*\\.html)",
"headers": [
{ "key": "Cache-Control", "value": "no-cache" }
]
}
]
}
5.3 Express / Next.js
// 自定义中间件
app.use((req, res, next) => {
if (req.path.match(/\.(js|css|woff2)$/)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
}
next()
})
Next.js 默认对 _next/static/ 加 immutable。
6. CDN 与浏览器缓存协作
Cache-Control: public, max-age=60, s-maxage=3600
- 浏览器缓存 60s
- CDN 缓存 1h
- 用户高频访问走浏览器缓存
- 跨用户走 CDN
- 1h 后 CDN 回源
7. 缓存失效场景
7.1 hash 文件名 + immutable(推荐)
app.abc123.js (永久缓存)
app.def456.js (新版,新 hash)
index.html (no-cache,每次验证)
发版只刷 HTML,JS 自然分流。
7.2 版本 query
<script src="/app.js?v=20260618"></script>
简单但部分 CDN 默认不区分 query,要配置缓存键含 query。
7.3 主动 purge
CDN API 刷新指定 URL。配额有限,不要日常依赖。
8. Service Worker 缓存
详见专门一篇。SW 缓存优先级最高,可拦截所有请求。SW 注册后浏览器 HTTP 缓存对它请求无效,要在 SW 内部实现缓存策略。
9. 故障排查
# 看响应头
curl -I https://example.com/app.js
# 第二次请求带条件头
curl -I -H "If-None-Match: \"abc123\"" https://example.com/app.js
# 返回 304 Not Modified
# Chrome DevTools
# Network → 单击请求 → Headers + Response 看 Cache-Control / ETag
# Network → "Disable cache" 关闭缓存调试
# Application → Storage → Clear site data 清缓存
9.1 用户看不到新版本
- 浏览器强缓存还在 → HTML 不应该长缓存
- CDN 没刷新
- Service Worker 缓存了旧版本
9.2 缓存策略没生效
add_header在 location 多层会被覆盖(Nginx)- 中间代理改了头
- Cookie / Authorization 头默认让响应不缓存
10. 常见反模式
- HTML max-age 1 小时:发版 1 小时内用户拿不到
- JS 不带 hash + 长缓存:发版用户老 JS + 新 HTML 不兼容
no-cache当不缓存:实际是协商缓存。要no-store- CDN 规则覆盖源站头:源站 immutable 被 CDN 改成 1 小时
- 登录态 API 长缓存:用户互相看到对方数据
- CORS 带 cookie 不加 Vary: Origin:第一个用户响应缓存给所有人
- 不用 immutable:浏览器仍发条件请求,浪费 RTT
11. 延伸阅读
- HTTP Cache RFC 9111
- web.dev: HTTP Cache
- MDN: Cache-Control
- 模块 03 Nginx 静态资源缓存
- 模块 02 CDN 工作原理