不喜欢广告? 无广告 今天

HTTP 缓存控制头 no-cache、no-store 和 max-age 实际上意味着什么

更新于

缓存控制指令的实用解析——浏览器和CDN实际上对no-cache、no-store、max-age、s-maxage和ETag的处理方式。包括在生产环境中最常困扰开发者的错误。

HTTP 缓存控制头:no-cache、no-store 和 max-age 实际含义 1
广告 移除?

你可能已经写过 Cache-Control: no-cache 并假设浏览器会完全跳过缓存。事实并非如此。 no-cache 表示“在从缓存提供之前必须重新验证。”如果你实际上希望响应永远不会被存储,那么应使用 no-store。这种误解会导致生产环境中的过时数据错误,这些问题很难诊断,因为在首次加载时网络标签看起来一切正常。

我们来逐一明确每个指令——浏览器如何处理它,CDN/代理如何处理它,以及因混淆而产生的错误。

完整的缓存控制指令参考

指令浏览器行为CDN / 代理行为典型用途
max-age=N缓存 N 秒缓存 N 秒(除非 s-maxage 覆盖)静态资源、API 响应
s-maxage=N被忽略缓存 N 秒CDN 的 TTL 与浏览器分离
no-cache缓存但每次请求都重新验证缓存但每次请求都重新验证频繁变化的内容配合 ETag
no-store不存储任何地方不存储任何地方认证响应、敏感用户数据
must-revalidate不提供过时内容——重新验证或失败不提供过时内容——重新验证或失败API 响应,过时即失效
proxy-revalidate被忽略在共享缓存中不提供过时内容CDN 特有的必须重新验证
private浏览器可以缓存不得缓存用户特定页面
public任何缓存都可以存储可以缓存共享静态资源
immutable在最大年龄内永不重新验证因 CDN 而异哈希/版本化的资产
stale-while-revalidate=N在获取新鲜内容的同时,提供 N 秒的过时内容因 CDN 而异在不产生硬过时的情况下提升速度

max-age 和 s-maxage:浏览器与 CDN 的 TTL

max-age=N 告诉浏览器和 CDN 响应在多少秒内是新鲜的。N 秒后,缓存的响应变为过时,必须重新验证后才能使用。

s-maxage=N 是 CDN 专用的。浏览器完全忽略它。如果你想让 CDN 缓存一小时,而浏览器只缓存 5 分钟:

Cache-Control: max-age=300, s-maxage=3600

浏览器缓存 5 分钟。CloudFront、Fastly、Nginx 等会使用 3600 秒。一个常见陷阱:设置 max-age=0 以为它会禁用缓存。实际上并不会。 max-age=0 表示响应立即过时——浏览器仍然会缓存它并重新验证,很可能会收到 304 响应。如果你永远不想缓存它,请使用 no-store.

no-cache:并非你以为的那样

Cache-Control: no-cache 并不表示“不要使用缓存”。它表示“在从缓存提供之前,必须先与服务器重新验证。”

当浏览器拥有一个带有 no-cache:

  1. 的缓存响应时,其序列如下:
  2. 新请求到达该缓存资源 If-None-Match浏览器将缓存响应的 ETag(通过
  3. )或 Last-Modified 时间戳发送给服务器 304 Not Modified 如果内容未改变 → 服务器返回
  4. ,无响应体 200 OK 如果内容已改变 → 服务器返回

,带有新的响应 no-store相比 no-cache 的优势在于:你仍然可以获得 304 响应带来的带宽节省。如果你的内容偶尔会变化,但变化时必须是最新状态,

结合 ETag 是正确的组合。

Cache-Control: no-store no-store:真正的“不要缓存”

表示响应不得在任何地方存储——既不在浏览器缓存,也不在 CDN 或中间代理中。没有副本,绝对禁止。

  • 适用于:
  • 认证响应(登录令牌、会话数据)
  • 敏感个人数据

一次性内容(支付确认、一次性验证码页面) no-store 一个细微之处: pageshow 不会阻止页面出现在浏览器的前进/后退缓存(bfcache)中。浏览器为导航性能保留一个内存快照,该快照与 HTTP 缓存是分开的。如果你需要处理登出后按后退按钮的问题,请监听 event.persisted.

事件并检查

必须重新验证:无过时宽容 must-revalidate 移除了这种宽容:一旦缓存响应过时,缓存必须重新验证或返回 504 错误。任何情况下都不允许提供过时内容。

# Without must-revalidate: CDN may serve stale if origin is slow or down
Cache-Control: max-age=3600

# With must-revalidate: stale = error, not a fallback
Cache-Control: max-age=3600, must-revalidate

适用于 API 响应,其中提供过时数据会破坏功能——例如库存数量、价格、认证状态,而不仅仅是看起来稍有不同。

private 与 public:CDN 中导致用户数据泄露的 bug

private 表示响应是为特定用户准备的。浏览器可以缓存它,但共享缓存(CDN、反向代理)必须不能缓存。

public 明确允许任何缓存——包括共享缓存——存储该响应。某些缓存仅在你明确标记时才会缓存经过身份验证的请求响应 public.

现实中的这个 bug:开发者将 Cache-Control: public, max-age=3600 从静态资源复制到包含用户特定数据的页面上。CDN 缓存了该响应。用户 B 发起相同请求,从缓存中获取了用户 A 的页面。这不是理论问题——GitHub 在 2018 年就曾出现过类似情况。请明确标记认证或用户特定的响应 private ,即使你认为你的 CDN “知道”不应缓存它们。

ETag 和条件请求

ETag 是服务器用来表示“这是响应的指纹”的方式。浏览器将 ETag 与缓存的响应一起存储,并在下一次请求时通过 If-None-Match发送回去。如果内容未改变,服务器返回 304 Not Modified ,无响应体——与 no-cache相同的 freshness 验证,节省大量带宽。

no-cache + ETag 流程:

→ GET /api/config HTTP/1.1

← HTTP/1.1 200 OK
   Cache-Control: no-cache
   ETag: "abc123"
   [full response body]

→ GET /api/config HTTP/1.1
   If-None-Match: "abc123"

← HTTP/1.1 304 Not Modified
   [no body — browser uses its cached copy]

两种 ETag 类型:

  • 强 ETag ("abc123")——字节完全一致。对于 CDN 的范围请求支持是必需的。
  • 弱 ETag (W/"abc123")——语义等价但不一定字节一致。适用于浏览器重新验证,不适用于范围请求。

Nginx 会自动从文件的修改时间和大小生成 ETag。Express 默认不添加 ETag——需使用 app.set('etag', 'strong')etag 中间件显式配置。

Last-Modified 和 If-Modified-Since

与 ETag 的概念相同,但更粗略——基于时间戳而非内容哈希。服务器包含 Last-Modified;浏览器在后续请求中发送 If-Modified-Since

问题:如果你重新部署,文件修改时间更新但内容未改变,缓存会不必要地失效。基于内容哈希的 ETag 就不会遇到这个问题。尽可能使用 ETag,将 Last-Modified 作为不支持 ETag 的服务器的后备方案。

Vary:悄悄增加你缓存数量的头部

Vary 头部告诉缓存,响应可能因其他请求头部而不同。每个这些头部的唯一组合都会得到一个独立的缓存条目。

Vary: Accept-Encoding

这告诉缓存为 gzip、brotli 和身份编码分别存储不同的响应。这是正确且常见的。危险的情况是: Vary: Cookie。每个用户都有唯一的 Cookie,因此每个用户都会获得自己的缓存条目——实际上禁用了共享缓存。许多框架会静默添加 Vary: Cookie 。如果你的 CDN 缓存命中率在没有合理 max-age 值的情况下异常低,请检查响应头部是否存在 Vary: Cookie 从会话中间件悄悄渗入的情况。

Vary: * 实际上意味着“完全不缓存”——每个请求都被视为唯一。对于 CDN 来说,这等同于 no-store

使用查询参数进行缓存破坏

当你需要强制下载版本化的资产时,追加查询参数是标准做法——查询字符串是 URL 的一部分,因此浏览器和 CDN 都将其视为新资源:

/app.js?v=2.1.4
/styles.css?hash=a1b2c3d4e5f6

如果你动态构建缓存破坏参数,这些参数来自内容哈希或版本字符串,可能包含特殊字符,请确保在追加前对其进行百分号编码。如果手动测试或构建 URL, URL编码器/解码器 可以快速处理该问题。

三个最常困扰开发者的错误

1. 使用 no-cache 而实际想使用 no-store。 如果你处理的是认证响应、登出端点或包含 PII 的内容,你应该使用 no-store. no-cache 仅将数据留在浏览器缓存中(标记为过时); no-store 则会彻底移除数据痕迹。当用户共享设备时,这种区别至关重要。

2. 未为 CDN 设置 s-maxage。 在此处粘贴 .github/workflows/*.yml 内容 s-maxage,你的 CDN 会使用 max-age。如果 max-age 是浏览器新鲜度的短时间(例如 60 秒),那么 CDN 也会缓存 60 秒——这可能不是你想要的结果。明确分离这两个 TTL。

3. 在返回用户数据的端点上使用 public。 这是安全事件,而不仅仅是性能问题。任何个性化或经过身份验证的响应都应为 private。默认为 private ,仅在真正共享的资源上才使用 public

整合应用

思维模型: no-cache 是关于新鲜度控制——响应存在于缓存中,只需在使用前获得服务器的批准即可。 no-store 是关于不留痕迹。 max-age 是浏览器的 TTL。 s-maxage 是 CDN 的独立 TTL。ETag 是实现廉价重新验证的关键。

在任何涉及用户数据的端点上,正确区分 private/public 。这个单一错误——将静态资源的缓存头复制到认证端点——一旦 CDN 开始缓存个性化响应,就会演变为跨用户数据泄露。

想要享受无广告的体验吗? 立即无广告

安装我们的扩展

将 IO 工具添加到您最喜欢的浏览器,以便即时访问和更快地搜索

添加 Chrome 扩展程序 添加 边缘延伸 添加 Firefox 扩展 添加 Opera 扩展

记分板已到达!

记分板 是一种有趣的跟踪您游戏的方式,所有数据都存储在您的浏览器中。更多功能即将推出!

广告 移除?
广告 移除?
广告 移除?

新闻角 包含技术亮点

参与其中

帮助我们继续提供有价值的免费工具

给我买杯咖啡
广告 移除?