HTTP 缓存头 缓存控制、ETag 和 max-age,无需猜测
为网页开发者编写的一份关于HTTP缓存头的实用指南:Cache-Control指令实际上做了什么,ETag如何触发304重新验证,如何选择在部署中仍能保持有效的TTL,以及那些使缓存反而有害而非有益的常见错误。
您发送的每个HTTP响应要么被缓存,要么没有被缓存——如果您对此不加刻意考虑,浏览器会自行决定。结果通常是,对于频繁变化的内容,缓存非常激进,而对于很少变化的内容,则完全不缓存。这两种情况都会损害用户体验。
本指南为您提供正确的设置HTTP缓存头的思维模型:每个指令控制什么,ETag如何触发重新验证,以及如何选择能够经受部署变化且不会提供过时内容的TTL。
浏览器缓存的实际工作原理
当浏览器请求一个资源时,它首先检查本地缓存。如果存在一个新鲜的缓存副本,它会立即提供该副本——根本不需要网络请求。如果缓存副本可能已过期,它会发送一个 条件请求 到源服务器。源服务器要么确认资源未发生变化(304 Not Modified),要么发送完整的更新响应(200 OK)。
CDN位于这个生命周期的中间位置。它们将响应更靠近用户地理区域进行缓存,并遵循相同的HTTP缓存头——带有少数CDN特定的扩展,比如 s-maxage.
三个问题决定了缓存行为:
- 该响应是否可以被缓存? 由
Cache-Control: no-store或private - 它有多新鲜? 由
max-age或s-maxage - 如何验证过期状态? 由ETag或
Last-Modified
Cache-Control头指令控制
这 Cache-Control 头是声明缓存策略的主要方式。多个指令以逗号分隔。以下是每个指令的实际作用:
max-age
max-age=N 告诉缓存(浏览器和CDN)响应在多长时间内保持新鲜。一个带有 max-age=86400 的响应从接收到时起恰好24小时是新鲜的。之后,缓存必须重新验证才能再次提供该响应。
对于带有版本化文件名的静态资源(如 main.abc123.js),一年是常见的: max-age=31536000。对于HTML文档,一个较短的时间窗口——甚至完全不缓存——更安全,因为HTML文档引用了这些版本化的资源。
s-maxage
s-maxage 覆盖 max-age 仅适用于共享缓存(CDN、代理服务器)。浏览器会忽略它。这使得您可以为用户提供长时间缓存的响应,同时保持CDN边缘更新鲜。一个典型模式是 Cache-Control: public, max-age=3600, s-maxage=86400 ——浏览器缓存1小时,CDN缓存24小时。
no-cache
no-cache 并不意味着“不要缓存”。它意味着缓存必须在提供存储响应之前与源服务器重新验证,即使该响应仍然新鲜。响应被缓存,但每次使用都需要一次往返请求来验证其有效性。这适用于频繁变化但能从304响应中获得带宽节省的内容。
no-store
no-store 是唯一一个真正阻止缓存的指令。没有浏览器缓存,没有CDN缓存,也没有磁盘写入。将其用于包含敏感用户数据的响应——如银行对账单、医疗记录、令牌。不要将其作为默认设置,因为您尚未认真考虑过缓存问题。
public 和 private
public 明确允许共享缓存(CDN)缓存响应,即使请求带有 Authorization 标头中来实现。 private 限制缓存仅限于终端用户的浏览器——CDN不得缓存它。对于用户特定的认证响应, private 防止一个用户的响应被提供给另一个用户。
must-revalidate
must-revalidate 防止缓存提供过期响应,当它们无法连接到源服务器时。如果没有它,缓存可能会在网络不可用时提供过期响应。有了它,如果源服务器不可达,请求将返回504错误。适用于提供过期数据比错误更糟糕的内容。
ETags:精确重新验证
ETag是服务器为资源特定版本生成的标识符——可以将其视为响应体的指纹。服务器在响应中发送它:
ETag: "abc123def456"
当缓存副本过期时,浏览器会发送带有存储ETag的条件GET请求:
If-None-Match: "abc123def456"
如果资源未发生变化,服务器会返回 304 Not Modified 并返回空体——节省带宽同时确认新鲜度。如果资源已更改,服务器会返回 200 OK 和新的ETag。
强ETag与弱ETag
A 强ETag ("abc123")意味着响应在字节级别上完全相同。一个 弱ETag (W/"abc123")意味着响应在语义上等价,但可能在细微方面有所不同,比如空格或头部顺序。弱ETag可以更高效地生成,但不能用于范围请求。除非您有特定使用弱ETag的理由,否则请使用强ETag。
Last-Modified:较旧的替代方案
在ETag出现之前,服务器使用 Last-Modified 时间戳进行重新验证。服务器发送:
Last-Modified: Thu, 01 May 2026 12:00:00 GMT
浏览器通过:
If-Modified-Since: Thu, 01 May 2026 12:00:00 GMT
重新验证。如果资源自该时间戳以来未更改,服务器将返回304。
缺点:时间戳的精度为一秒。在同一个秒内修改两次的文件将被视为未更改。ETag能正确处理这种情况,是更优的选择。大多数现代框架同时发送两者——浏览器会使用可用的任一选项,ETag优先。
在不破坏部署的情况下实现缓存破坏
静态资产上的一个长 max-age 只有在内容变化时URL也发生变化时才安全。存在两种策略:
URL指纹法(推荐)
将文件内容的哈希值包含在文件名中: main.a1b2c3d4.js。当文件更改时,哈希值变化,URL变化,浏览器会获取新文件——完全绕过缓存。旧URL仍保留在缓存中,但一旦HTML引用了新URL,就不会再被请求。
Webpack、Vite和大多数现代打包工具会自动执行此操作。该模式允许您安全地设置 Cache-Control: public, max-age=31536000, immutable —— immutable 指令告诉浏览器即使缓存条目技术上已过期,也不必重新验证。
查询字符串(可靠性较低)
通过在URL末尾附加版本号作为查询字符串(main.js?v=1.2.3)技术上创建了不同的URL,但一些CDN和代理服务器在构建缓存键时会忽略查询字符串。路径中的URL指纹法更可靠且被普遍支持。
会损害缓存的常见错误
缓存不应被缓存的API响应
返回用户特定或时间敏感数据的JSON API应使用 Cache-Control: no-store 或至少 private, no-cache。一个常见错误是让CDN缓存类似 /api/user/profile 的响应,并向另一个用户提供其数据。如果您的API未设置 Cache-Control 头,一些CDN会使用启发式方法自行缓存它。
忘记设置 Vary: Accept-Encoding
如果您的服务器根据客户端的 Accept-Encoding 头提供压缩和未压缩版本的资源,则缓存必须为每个变体存储独立副本。如果没有 Vary: Accept-Encoding,CDN可能会缓存gzip版本并为不支持gzip的客户端提供,或反之。在压缩是条件性的时,始终设置它。
使用 no-cache 而实际想使用 no-store
开发者经常写 Cache-Control: no-cache 当他们希望完全阻止缓存时,但 no-cache 仍然会存储响应——它只是在每次使用前都需要重新验证。当您确实不希望响应在任何地方持久化时,请使用 no-store 。
在HTML上设置 max-age 但没有缓存破坏策略
HTML文档引用了版本化的资产。如果您用长 max-age缓存HTML,用户在部署后将不会获取到新的资产文件名——他们将继续运行旧的HTML,而该HTML引用了旧的哈希值。为HTML设置较短的TTL(或 no-cache),并将长TTL保留给HTML引用的不可变资产。
在发布前计算您的过期窗口
选择 max-age 值在您能将其可视化为实际的墙钟时间时更容易。iotools.cloud上的 HTTP 缓存 TTL / max-age 计算器 允许您以秒为单位输入TTL并查看确切的过期时间。在将值提交到服务器配置或CDN规则之前,可用于检查86400(24小时)、2592000(30天)或31536000(1年)等值的合理性。
实用的缓存策略检查清单
- HTML文档:
Cache-Control: no-cache——始终重新验证,当内容未改变时受益于304响应 - 带有哈希值在文件名中的版本化静态资产(JS、CSS、图片):
Cache-Control: public, max-age=31536000, immutable - 未版本化的静态资产(字体、图标):
Cache-Control: public, max-age=604800(1周) - 公共API响应(时间敏感):
Cache-Control: public, max-age=60, s-maxage=300——浏览器短TTL,CDN长TTL - 用户特定的API响应:
Cache-Control: private, no-cache - 敏感数据:
Cache-Control: no-store - 始终设置
Vary: Accept-Encoding当提供条件性压缩响应时
ETags应默认开启,用于所有被缓存的内容——它们是实现高效带宽重新验证的机制。大多数Web服务器(Nginx、Apache、Caddy)在未禁用的情况下会自动生成ETags。
