Web 缓存策略:强缓存、协商缓存与 CDN 三层
缓存配好了,用户第二次访问几乎零请求;配错了,要么用户看不到更新,要么每次都重新下载。核心就一件事:区分「会不会变」。
一、三层缓存
一次请求会经过三层,每层都能拦下来:
| 层 | 位置 | 控制方式 |
|---|---|---|
| 浏览器缓存 | 用户本机 | 响应头 Cache-Control / Expires |
| CDN 缓存 | Cloudflare 等边缘节点 | CDN 控制台规则 |
| 源站缓存 | Nginx / 应用 | proxy_cache、应用内缓存 |
越靠前拦截,越快也越省带宽。目标是让静态资源在第一层就被拦下。
二、强缓存:不问服务器直接用
浏览器看到 Cache-Control: max-age=31536000,在有效期内根本不发请求,直接用本地副本。DevTools 里会显示 (from disk cache)。
# 文件名带 hash(如 app.a1b2c3d4.js)—— 内容变了文件名就变,可以放心长缓存
location ~* .[0-9a-f]{8,}.(css|js)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}关键字段:
| 字段 | 含义 |
|---|---|
max-age=N | 缓存 N 秒 |
public | 允许任何中间层(CDN)缓存 |
private | 只允许浏览器缓存,CDN 不缓存 |
no-cache | 可以缓存,但每次都要去服务器确认 |
no-store | 完全不缓存(敏感数据用) |
immutable | 有效期内即使用户刷新也不发请求 |
no-cache的名字很有误导性:它不是「不缓存」,而是「缓存但必须重新验证」。真正不缓存是no-store。
三、协商缓存:问一下还能不能用
缓存过期后,浏览器带一个「指纹」去问服务器:内容变了吗?没变就返回 304 Not Modified,body 不传,省下传输。
两对指纹字段:
| 请求头 | 响应头 | 比较方式 |
|---|---|---|
If-None-Match | ETag | 内容哈希,精确 |
If-Modified-Since | Last-Modified | 时间戳,秒级精度 |
ETag 更可靠(内容没变但 mtime 变了的情况很常见),Last-Modified 只能到秒、且无法感知「改回原样」。
# 看 ETag 与验证过程
curl -sI https://www.bzii.cn/app.js | grep -i etag
# 带上指纹再请求,服务器返回 304
ETAG=$(curl -sI https://www.bzii.cn/app.js | grep -i etag | awk "{print $2}" | tr -d "\r")
curl -sI -H "If-None-Match: $ETAG" https://www.bzii.cn/app.js | head -1
# HTTP/1.1 304 Not ModifiedNginx 默认就给静态文件生成 ETag,不用额外配置。
四、策略矩阵:按资源类型给
这是我觉得最实用的一张表,直接抄:
| 资源 | 策略 | 理由 |
|---|---|---|
| 带 hash 的 JS/CSS | max-age=1y, immutable | 内容变则文件名变,永不过期 |
| 不带 hash 的 JS/CSS | no-cache + ETag | 每次验证,保证能更新 |
| 图片/字体 | max-age=30d | 很少变,可适当长 |
| index.html | no-cache(或 max-age 很短) | 入口文件必须及时更新 |
| API 响应 | no-store 或 private, max-age=0 | 数据实时性优先 |
| 用户隐私页 | no-store | 绝不能留在缓存里 |
# 入口 HTML:必须及时更新,否则发新版用户看不到
location = /index.html {
add_header Cache-Control "no-cache";
}
# 普通静态资源
location ~* .(png|jpg|jpeg|gif|webp|svg|woff2)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}五、为什么文件名 hash 是前提
这套策略成立的条件是:文件内容变了,URL 必须变。
- 有 hash:
app.a1b2.js→app.c3d4.js,新 URL 必然重新下载,旧的可以永久缓存; - 无 hash:文件名永远叫
app.js,你只能设很短的 max-age,或者用 no-cache 每次验证。
所以零构建的小站点可以直接给资源加版本号参数:
<link rel="stylesheet" href="/style.css?v=20260914">
<script src="/app.js?v=20260914"></script>改版时手动改一下 v=,效果等同于 hash。
六、CDN 那一层要注意
CDN 看的是同样的响应头,但还有自己的规则:
- Cloudflare 免费版默认不缓存 HTML,只缓存静态扩展名;
- 想改要在 Cache Rules 里显式配置;
- 发版后记得清缓存,否则 HTML 或长缓存资源还是旧的。
# 看有没有命中 CDN 缓存
curl -sI https://www.bzii.cn/app.js | grep -i cf-cache-status
# HIT 命中边缘缓存,最快
# MISS 未命中,回源了
# BYPASS 规则要求跳过缓存
# DYNAMIC 内容被判定为动态,不缓存七、一次完整的验证流程
# 1. 看响应头里缓存策略对不对
curl -sI https://www.bzii.cn/style.css | grep -i cache-control
# 2. 改一个文件重新部署,确认 URL 变了(或有 304 机制兜底)
# 3. 强制刷新验证(浏览器 Ctrl+Shift+R 会绕过强缓存)
# 4. 用无痕窗口模拟「新用户」首次访问八、最常见的两个坑
- HTML 被长缓存 → 发新版用户看到的还是旧页面,还以为部署失败。解法:HTML 一律 no-cache;
- 改了 CSS 但没改文件名 → 用户浏览器还用旧缓存,页面样式错乱。解法:加版本号或 hash。
判断口诀:会不会变?会变就别缓存(或每次验证),不会变就往死里缓存。
