Web 缓存策略:强缓存、协商缓存与 CDN 三层

缓存配好了,用户第二次访问几乎零请求;配错了,要么用户看不到更新,要么每次都重新下载。核心就一件事:区分「会不会变」

一、三层缓存

一次请求会经过三层,每层都能拦下来:

位置控制方式
浏览器缓存用户本机响应头 Cache-Control / Expires
CDN 缓存Cloudflare 等边缘节点CDN 控制台规则
源站缓存Nginx / 应用proxy_cache、应用内缓存

越靠前拦截,越快也越省带宽。目标是让静态资源在第一层就被拦下。

二、强缓存:不问服务器直接用

浏览器看到 Cache-Control: max-age=31536000,在有效期内根本不发请求,直接用本地副本。DevTools 里会显示 (from disk cache)

nginx
# 文件名带 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-MatchETag内容哈希,精确
If-Modified-SinceLast-Modified时间戳,秒级精度

ETag 更可靠(内容没变但 mtime 变了的情况很常见),Last-Modified 只能到秒、且无法感知「改回原样」。

bash
# 看 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 Modified

Nginx 默认就给静态文件生成 ETag,不用额外配置。

四、策略矩阵:按资源类型给

这是我觉得最实用的一张表,直接抄:

资源策略理由
带 hash 的 JS/CSSmax-age=1y, immutable内容变则文件名变,永不过期
不带 hash 的 JS/CSSno-cache + ETag每次验证,保证能更新
图片/字体max-age=30d很少变,可适当长
index.htmlno-cache(或 max-age 很短)入口文件必须及时更新
API 响应no-storeprivate, max-age=0数据实时性优先
用户隐私页no-store绝不能留在缓存里
nginx
# 入口 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.jsapp.c3d4.js,新 URL 必然重新下载,旧的可以永久缓存;
  • 无 hash:文件名永远叫 app.js,你只能设很短的 max-age,或者用 no-cache 每次验证。

所以零构建的小站点可以直接给资源加版本号参数:

html
<link rel="stylesheet" href="/style.css?v=20260914">
<script src="/app.js?v=20260914"></script>

改版时手动改一下 v=,效果等同于 hash。

六、CDN 那一层要注意

CDN 看的是同样的响应头,但还有自己的规则:

  • Cloudflare 免费版默认不缓存 HTML,只缓存静态扩展名;
  • 想改要在 Cache Rules 里显式配置;
  • 发版后记得清缓存,否则 HTML 或长缓存资源还是旧的。
bash
# 看有没有命中 CDN 缓存
curl -sI https://www.bzii.cn/app.js | grep -i cf-cache-status
# HIT   命中边缘缓存,最快
# MISS  未命中,回源了
# BYPASS 规则要求跳过缓存
# DYNAMIC 内容被判定为动态,不缓存

七、一次完整的验证流程

bash
# 1. 看响应头里缓存策略对不对
curl -sI https://www.bzii.cn/style.css | grep -i cache-control

# 2. 改一个文件重新部署,确认 URL 变了(或有 304 机制兜底)

# 3. 强制刷新验证(浏览器 Ctrl+Shift+R 会绕过强缓存)

# 4. 用无痕窗口模拟「新用户」首次访问

八、最常见的两个坑

  1. HTML 被长缓存 → 发新版用户看到的还是旧页面,还以为部署失败。解法:HTML 一律 no-cache;
  2. 改了 CSS 但没改文件名 → 用户浏览器还用旧缓存,页面样式错乱。解法:加版本号或 hash。

判断口诀:会不会变?会变就别缓存(或每次验证),不会变就往死里缓存。