2. max-age指令:含义、语法、优先级、与Expires对比

概念解释

max-age 是HTTP Cache-Control 响应头中最核心的指令之一,它定义了一个相对时间,以秒为单位指定资源在缓存中可以被视为“新鲜”的最大时长。当缓存收到携带 max-age 的响应后,从响应生成时刻开始计时,在此时间段内可直接复用缓存,无需回源验证。

  • 相对时间:客户端或代理缓存无需关心服务器时钟,只需用本地时间倒计时。
  • 精准控制:相比 Expires(时间),max-age 避免了服务器与客户端时钟不同步的问题。
  • 作用域:仅影响私有缓存(浏览器)和共享缓存(CDN/代理),可通过 s-maxage 覆盖。

语法

指令格式如下:

Cache-Control: max-age=<seconds>

其中 <seconds> 必须是非负整数,例如 3600 表示1小时。可以与其他指令并用:

Cache-Control: public, max-age=86400, must-revalidate
Cache-Control: private, max-age=300

注意:max-age=0 表示资源立即过期,并非禁止缓存,而是要求每次使用前必须向服务器验证(通常配合 ETag 或 Last-Modified)。

优先级规则

当一个响应中包含多个与过期时间相关的指令时,遵循以下优先顺序(从高到低):

  1. Cache-Control: s-maxage(仅共享缓存)
  2. Cache-Control: max-age(覆盖 Expires)
  3. Expires(HTTP/1.0 传统头,低优先级)

若同时存在 max-age 和 Expires,HTTP/1.1 规范规定 max-age 优先,Expires 被忽略。

⚠️ 常见误解:不要在响应中同时使用 Cache-Control: max-age=3600 和 Expires: <未来1小时的时间>。虽然 max-age 生效,但冗余头会增加混淆和调试成本。

与Expires对比

维度 max-age Expires
时间类型 相对时间(秒) 时间(HTTP日期格式)
时钟同步依赖 只依赖客户端本地时钟的计时精度,不依赖服务器时钟 依赖客户端和服务器时钟一致,常见NTP偏差导致缓存失效或提前
HTTP版本 HTTP/1.1(推荐) HTTP/1.0(遗留支持)
精度 秒级精确 秒级,但完整格式冗长(如 Expires: Thu, 01 Dec 2025 16:00:00 GMT)
可否覆盖 可通过 s-maxage 为共享缓存单独设置 无法被单独覆盖,只能整体替换
优先级 高于 Expires 被 max-age 或 s-maxage 覆盖时无效
💡 最佳实践:现代Web应用应完全依赖 Cache-Control: max-age,废弃 Expires。仅保留 Expires 作为兼容HTTP/1.0的降级方案。

原理图解:缓存过期决策流程

Cache-Control max-age 与 Expires 优先级决策流程图 缓存过期决策:max-age 与 Expires 优先级 收到服务器响应 是否包含 Cache-Control: max-age? (检查响应头) 是 使用 max-age 过期时间 = 响应时间 + max-age 否 检查 Expires 头 过期时间 = Expires 值 缓存是否有效? 当前时间 ≤ 过期时间 有效 直接使用缓存 无效 回源验证 / 重新请求 优先级说明 max-age 优先于 Expires, 且 s-maxage 覆盖 max-age(共享缓存)。

类比:面包保质期

  • Expires:就像面包包装上印着“请于 2025-05-20 前食用”——这是日期,如果冰箱上的日历不准,你可能提前或推迟判断。
  • max-age:就像超市收银条上写着“烘烤时间后保质 72 小时”——从你拿到面包的那一刻开始计时,不管你家的钟快慢,只需倒计时即可。

在网络环境中,客户端时钟的偏差非常常见(如用户设置的时区错误、虚拟机时间跳跃),因此 max-age 的“相对计时”更可靠。

常见误区

  1. 误区:max-age=0 就是禁止缓存
    真相:max-age=0 只是让缓存立即过期,但资源仍会被缓存(放入磁盘或内存中),下一次请求时必须携带条件验证头(If-None-Match / If-Modified-Since)去服务器判断。要真正禁止缓存,应使用 Cache-Control: no-store。
  2. 误区:Cache-Control: max-age=3600 与 Expires 同时使用更安全
    真相:HTTP/1.1 规范明确指定 max-age 覆盖 Expires,两者同时存在时 Expires 被忽略。过度添加冗余头反而可能造成老旧代理的解析冲突。
  3. 误区:max-age 在私有缓存和共享缓存中行为完全一致
    真相:私有缓存(浏览器)和共享缓存(CDN)都遵守 max-age,但共享缓存还可以通过 s-maxage 单独覆盖 max-age。如果希望CDN缓存更长但浏览器缓存更短,必须使用 s-maxage 和 max-age 的组合。
  4. 误区:max-age 可以控制旧响应不被重新使用
    真相:max-age 只控制“新鲜度”,不控制“存储”。即使 max-age=0,缓存(尤其浏览器)仍可能因为“启发式缓存”或“history cache”在无验证下复用陈旧响应(例如用户点击“后退”按钮)。

练习题

✏️ 思考题:某资源响应头同时包含以下两行:
Cache-Control: public, max-age=300
Expires: Mon, 01 Jan 2025 00:00:00 GMT
服务器实际生成响应的时间是 2025-01-01 00:05:00(UTC)。客户端时钟比服务器快 10 分钟(即客户端认为当前时间是 2025-01-01 00:15:00)。请问:
1)客户端收到响应后,缓存会认为该资源在多长时间内有效?
2)如果在 2025-01-01 00:08:00(客户端本地时间)请求该资源,缓存会直接使用还是回源?

答案提示:由于 max-age=300 优先于 Expires,缓存从响应生成时刻(服务器时间 00:05:00)计时,有效期至 00:10:00(服务器时间)。客户端时钟偏差不影响相对计时,但客户端需要根据本地时间倒计时,若本地时间 00:15:00 已超过 00:05:00 + 300s = 00:10:00,则资源已过期。第一个子问题:有效期为 300 秒。第二个子问题:客户端本地 00:08:00 相当于服务器时间 00:08:00 - 10min = 2024-12-31 23:58:00?注意偏差计算,实际上 max-age 使用本地相对时间,只需检查自缓存存储后经过的秒数。若在 00:08:00 请求,距存储时刻仅 3 分钟(180秒)< 300秒,所以缓存有效,直接使用。

应用边界

  • 不能替代 no-cache 的条件验证:max-age 控制过期,但过期的资源仍可被有条件地重用(通过 ETag / Last-Modified)。若需强制每次回源,应使用 no-cache(不是禁止存储,而是强制验证)。
  • 共享缓存的特殊覆盖:当使用CDN或反向代理时,max-age 对共享缓存生效,但可以通过 Cache-Control: s-maxage=3600, max-age=600 使CDN缓存1小时,浏览器缓存10分钟——注意顺序:s-maxage 必须写在 max-age 之前才能覆盖。
  • 与 must-revalidate 配合:如果资源在过期后必须严格回源验证(不允许使用未验证的陈旧响应),应添加 must-revalidate 指令,但该指令不影响 max-age 本身的计时。
  • API 动态资源:对于不经常变化的静态资源(如图片、CSS、JS),设置较大的 max-age(如一年)并通过文件名 hash 控制更新;对于频繁变化的API响应,使用 max-age=0 配合验证头,实现“无需过期校验即可复用”的无脑缓存策略。