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)。
优先级规则
当一个响应中包含多个与过期时间相关的指令时,遵循以下优先顺序(从高到低):
- Cache-Control: s-maxage(仅共享缓存)
- Cache-Control: max-age(覆盖
Expires) - 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的降级方案。
原理图解:缓存过期决策流程
类比:面包保质期
- Expires:就像面包包装上印着“请于 2025-05-20 前食用”——这是日期,如果冰箱上的日历不准,你可能提前或推迟判断。
- max-age:就像超市收银条上写着“烘烤时间后保质 72 小时”——从你拿到面包的那一刻开始计时,不管你家的钟快慢,只需倒计时即可。
在网络环境中,客户端时钟的偏差非常常见(如用户设置的时区错误、虚拟机时间跳跃),因此 max-age 的“相对计时”更可靠。
常见误区
- 误区:
max-age=0就是禁止缓存
真相:max-age=0只是让缓存立即过期,但资源仍会被缓存(放入磁盘或内存中),下一次请求时必须携带条件验证头(If-None-Match/If-Modified-Since)去服务器判断。要真正禁止缓存,应使用Cache-Control: no-store。 - 误区:
Cache-Control: max-age=3600与Expires同时使用更安全
真相:HTTP/1.1 规范明确指定max-age覆盖Expires,两者同时存在时Expires被忽略。过度添加冗余头反而可能造成老旧代理的解析冲突。 - 误区:
max-age在私有缓存和共享缓存中行为完全一致
真相:私有缓存(浏览器)和共享缓存(CDN)都遵守max-age,但共享缓存还可以通过s-maxage单独覆盖max-age。如果希望CDN缓存更长但浏览器缓存更短,必须使用s-maxage和max-age的组合。 - 误区:
max-age可以控制旧响应不被重新使用
真相:max-age只控制“新鲜度”,不控制“存储”。即使max-age=0,缓存(尤其浏览器)仍可能因为“启发式缓存”或“history cache”在无验证下复用陈旧响应(例如用户点击“后退”按钮)。
练习题
✏️ 思考题:某资源响应头同时包含以下两行:
1)客户端收到响应后,缓存会认为该资源在多长时间内有效?
2)如果在 2025-01-01 00:08:00(客户端本地时间)请求该资源,缓存会直接使用还是回源?
答案提示:由于
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配合验证头,实现“无需过期校验即可复用”的无脑缓存策略。