Cache-Control 缓存头怎么配:按资源类型给出浏览器与 CDN 配置表

博客 / 边缘与 CDN

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=2592000URL 不复用就可以放长
公共 API(排行榜、状态数据)public, max-age=0, s-maxage=30, stale-while-revalidate=60CDN 吸收突发流量,浏览器总拿较新数据
登录后的 API、个人页面private, no-cache浏览器可缓存但要确认,CDN 不许存
支付、令牌、敏感个人信息no-store哪一层都不留副本
robots.txt、sitemap.xmlpublic, max-age=3600改动不频繁,一小时足够

有两条容易漏:

  1. 不带 hash 的文件不要用 immutable。浏览器会在一年内都不再确认,改了文件也看不到。
  2. 响应里有 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 会让命中率大幅下降

一个排查顺序:

  1. 先看源站本身返回的头,绕过 CDN 直接请求源站(curl -sI --resolve example.com:443:源站IP https://example.com/路径),确认 Cache-Control 写对了。
  2. 再经 CDN 请求两到三次,看 cf-cache-status 是否从 MISS 变成 HIT,Age 是否出现。
  3. 一直是 MISS:检查是否有 Set-Cookie、private、no-store,或者查询参数每次都不一样(比如带时间戳)。
  4. 一直是 DYNAMIC:Cloudflare 默认只缓存按扩展名识别的静态文件,HTML 和 API 需要单独写缓存规则。
  5. 用户反馈看不到更新:看 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 的说明。

相关文章

继续阅读