- Published on
- 约 2752 字
深入理解 HTTP 启发式缓存:浏览器如何在没有 Cache-Control 时做决策
- Authors

- Name
- 小辉辉
一个让人困惑的场景
某天,你在 Chrome DevTools 的 Network 面板中看到了这样的响应头:
accept-ranges: bytes
content-length: 544043
content-type: image/png
date: Wed, 13 May 2026 07:06:26 GMT
etag: "ef64c8265e86ea52f7e5cc6feabff5d4"
last-modified: Sun, 10 May 2026 16:18:01 GMT
没有 Cache-Control,没有 Expires。按照常理,浏览器应该每次都会发起 If-None-Match 或 If-Modified-Since 的协商缓存请求,对吗?
但实际情况是:刷新页面后,请求显示 200 OK (from memory cache) —— 浏览器直接使用了缓存,根本没有发起任何请求!
这就是 启发式缓存(Heuristic Caching)在起作用。
什么是启发式缓存?
启发式缓存是 HTTP/1.1 规范(RFC 7234)中定义的一种后备机制。当响应缺少明确的缓存指令(Cache-Control 或 Expires)时,缓存(包括浏览器、代理服务器、CDN)可以根据已有信息推测一个合理的缓存过期时间。
RFC 7234 第 4.2.2 节的原话是:
If no explicit expiration time is present in the response, a heuristic expiration time might be computed.
翻译过来就是:没有明确过期时间,就可以推测一个出来。
⚠️ 重要限制:RFC 规定,启发式计算的时间不能超过从响应产生开始计算的 24 小时(即
Date头之后 24 小时)。但在实际实现中,各浏览器执行得并不严格。
浏览器如何计算启发式缓存时间?
Chrome 的实现算法
Chrome 使用了一个相对简单且激进的算法:
启发式过期时间 = (响应时间 - Last-Modified 时间) × 10%
其中:
- 响应时间 =
Date头的值(或收到响应的时间) - Last-Modified = 资源最后修改时间
一个真实案例的演算
假设响应头:
Last-Modified: Sun, 10 May 2026 16:18:01 GMTDate: Wed, 13 May 2026 07:06:26 GMT
计算步骤:
- 时间差 = 2026-05-13 07:06:26 - 2026-05-10 16:18:01 ≈ 2.62 天
- 转换为秒 ≈ 226,368 秒
- 应用公式:226,368 × 10% ≈ 22,637 秒
- 转换为小时:22,637 ÷ 3600 ≈ 6.29 小时
结论:这张图片在 Chrome 中的启发式缓存时长约为 6.3 小时。在此期间,刷新页面会直接从 (memory cache) 或 (disk cache) 读取,完全不会向服务器发送任何请求。
为什么是 10%?
这个比例背后有一种假设:如果一个资源很久没有修改,那么它未来也不太可能很快被修改。
举个例子:
- 一张图片上次修改是 100 天前 → 启发式缓存时长 ≈ 10 天
- 一个 CSS 文件 1 小时前刚改过 → 启发式缓存时长 ≈ 6 分钟
从统计学角度看,这个启发式规则在某些场景下是合理的,但它并不适用于所有情况。
不同浏览器的行为差异
Chrome(最激进)
- 始终应用启发式缓存(只要有
Last-Modified) - 使用公式:
(Date - Last-Modified) × 10% - 遵循 24 小时上限(但不是非常严格)
- 这也是为什么很多开发者“惊喜地”发现缓存没有按预期失效
Firefox(更保守)
- 也支持启发式缓存,但上限更严格
- 默认启发式上限 = 24 小时(符合 RFC 建议)
- 在某些版本中,首选的启发式缓存时长 =
(Date - Last-Modified) × 10%,但不超过 24 小时
Safari(最保守)
- 启发式缓存时间通常更短
- 更倾向于发起协商缓存请求(
If-Modified-Since) - 对非静态资源的启发式行为较弱
缓存类型对比
| 缓存类型 | 是否发请求 | 响应码 | 场景 |
|---|---|---|---|
from memory cache | ❌ 否 | 200(显示灰色) | 启发式/强缓存 + 内存充足 |
from disk cache | ❌ 否 | 200(灰色) | 启发式/强缓存 + 磁盘 |
304 Not Modified | ✅ 是(验证) | 304 | 协商缓存命中 |
200 OK | ✅ 是(完整) | 200 | 缓存未命中 |
启发式缓存的潜在风险
1. 内容更新后用户看不到最新版本
假设你更新了一个 CSS 文件:
- 旧文件
Last-Modified: 2026-05-10 - 今天(2026-05-13)用户首次访问,浏览器计算出 6.3 小时的启发式缓存
- 你在 1 小时后部署了新版本
- 用户在部署后的 5 小时再访问 → 仍然看到旧样式
2. 不同浏览器行为不一致
同一个网站,Chrome 用户可能看到“新鲜”的启发式缓存,而 Safari 用户每次都发验证请求。这会导致:
- 难以调试缓存问题
- 用户看到的内容不一致
3. 浪费存储空间
如果启发式缓存时间设置得过长(比如 10 天),而你的资源实际上可能在 1 天后就变了,那么缓存既占用了用户磁盘空间,又提供了过时的内容。
4. 对 CDN 和中间代理的影响
很多 CDN 也有自己的启发式缓存逻辑,可能与浏览器不同,导致:浏览器从磁盘缓存读取旧内容 → CDN 缓存也可能旧 → 多级缓存都不可控。
如何控制或禁用启发式缓存?
最佳方案是:不要依赖启发式缓存,而是提供明确的缓存策略。
方案一:明确的强缓存(推荐)
Cache-Control: max-age=3600
# 或者更完整的写法
Cache-Control: public, max-age=86400, immutable
方案二:每次都验证(禁用强制缓存)
Cache-Control: no-cache
# 或者
Cache-Control: max-age=0, must-revalidate
注意:no-cache 不是不缓存,而是要求每次使用缓存前都去服务器验证。no-store 才是真正的"不缓存"。
方案三:彻底阻止启发式计算
虽然不能在所有浏览器中完全"禁用"启发式逻辑,但你可以通过以下方式确保启发式缓存时间尽可能短:
Cache-Control: max-age=0
Expires: 0
或者在响应中添加 Cache-Control: private 和简短的最大有效期。
一个更保险的组合头
Cache-Control: max-age=3600, must-revalidate
Expires: Wed, 13 May 2026 08:06:26 GMT
Last-Modified: Sun, 10 May 2026 16:18:01 GMT
ETag: "ef64c8265e86ea52f7e5cc6feabff5d4"
这样:
- 支持 HTTP/1.1 的客户端优先使用
Cache-Control: max-age=3600 - 不支持 HTTP/1.1 的老客户端使用
Expires - 如果 max-age 过期,则通过
ETag/Last-Modified做协商缓存 - 启发式缓存不再被需要
如何检测你的网站是否正在使用启发式缓存?
Chrome DevTools 方法
- 打开 Network 面板
- 勾选 Disable cache(这是为了测试,正常用户不勾选)
- 刷新页面,观察资源的
Size列- 显示
(memory cache)或(disk cache)但状态码为 200 → 可能是启发式缓存
- 显示
- 点击某个资源 → Headers 标签页
- 如果看到
Status: 200 (from disk cache)但根本没有发送请求(Time 显示 0 ms 或 < 2 ms),那就是启发式缓存/强缓存
- 如果看到
判断窍门
- 有
Cache-Control: max-age→ 强缓存(预期行为) - 没有
Cache-Control,但有Last-Modified→ 很可能是启发式缓存 - 多个页面会话后,同一个资源仍然直接从缓存读取 → 启发式缓存(没有过期)
- 使用 Chrome 的
Clear site data清除缓存后再加载 → 如果再次看到直接缓存命中,说明启发式逻辑再次生效
实际案例:为什么 Nginx 默认配置如此危险?
很多人的 Nginx 或 Node.js(express.static)静态服务器配置如下:
location ~* \.(jpg|png|css|js)$ {
expires off; # 没有 Expires 头
# 也没有 add_header Cache-Control ...
add_header ETag on;
# 只有 Last-Modified 和 ETag
}
结果:
- 浏览器会使用启发式缓存
- 文件更新后很长时间(几小时到几天)用户看不到变化
- 用户反馈“清除了缓存也没用” → 因为启发式缓存时间较长
解决方案:明确配置 Cache-Control。
location ~* \.(css|js)$ {
expires 1h; # 等效于 Cache-Control: max-age=3600
add_header Cache-Control "public";
}
或针对频繁变化的文件:
location ~* \.(html)$ {
add_header Cache-Control "no-cache";
}
启发式缓存的历史与未来
历史背景
启发式缓存诞生于互联网早期(1990 年代),那时很多服务器不发送缓存头。设计者希望即使服务器配置不完善,浏览器也能合理且高效地利用缓存。
当时的主流模式是:
- 静态资源往往很少变化
- 带宽和性能昂贵
- 宁愿长期缓存也不愿意多一次请求
现在的情况
- 现代网站资源更新频繁(持续部署、热更新)
- 带宽更便宜但等待时间更敏感
- 缓存策略已成为前端性能优化的核心
未来趋势
- HTTP/3 + Cache Digests:允许客户端和服务端更高效地协商缓存状态
- Stale-While-Revalidate:提供在后台更新缓存的同时返回旧内容的能力
- 更严格的浏览器实现:Chrome 可能在将来降低启发式缓存的默认时长(目前已经有了限制 24 小时)
- 推荐开发者主动控制:Lighthouse 会警告“缺少明确的缓存策略”
值得遵守的最佳实践
1. 总是提供明确的 Cache-Control
# 不会变化的静态资源(带哈希值)
Cache-Control: public, max-age=31536000, immutable
# 会变化的资源
Cache-Control: public, max-age=3600
# HTML 等入口文件
Cache-Control: no-cache
2. 使用文件名哈希(Content Hashing)
不要依赖 Last-Modified 或启发式缓存进行版本控制。更好的方法:
<!-- 坏 -->
<link href="/style.css" />
<!-- 好 -->
<link href="/style.a3d8f2.css" />
配合 max-age=31536000(一年)。
3. 主动测试缓存行为
使用 curl 查看完整响应头:
curl -I https://example.com/image.png
确保看到明确的 Cache-Control 或 Expires。
4. 记录和监控
如果你的网站仍依赖启发式缓存,请:
- 在文档中指出
- 定期审查旧资源的启发式缓存时长
- 考虑添加
Cache-Control来替代它
总结
| 项目 | 说明 |
|---|---|
| 触发条件 | 响应没有 Cache-Control 和 Expires,但有 Last-Modified |
| Chrome 算法 | (Date - Last-Modified) × 10% |
| 实际表现 | 资源直接从 memory cache / disk cache 读取,不发任何请求 |
| 最大限制 | 24 小时(RFC 建议但浏览器实现不一) |
| 风险 | 内容更新后用户看不到最新版本,浏览器行为不一致 |
| 最佳方案 | 主动提供明确的 Cache-Control,不要依赖启发式缓存 |
一句话总结
启发式缓存是浏览器在没有明确指令时的一种“聪明猜测”,但它不够可靠,生产环境应该总是提供明确的
Cache-Control策略覆盖它。
检查清单
- 响应中有
Cache-Control吗? - 静态资源使用了文件名哈希吗?
- 使用
curl验证了缓存头? - 测试过浏览器刷新和硬刷新行为?
- CDN 或中间代理的缓存策略与浏览器一致?
参考资源
- RFC 7234: Hypertext Transfer Protocol (HTTP/1.1): Caching
- MDN: HTTP Caching - Heuristic Caching
- Chrome Source Code - Heuristic Caching Logic
- Lighthouse: Uses inefficient cache policy
你的网站还在依赖启发式缓存吗? 打开 Chrome DevTools 看看 Network 面板,说不定就会发现有资源显示了 (memory cache),而它的 Cache-Control 其实是缺失的。是的,浏览器比你想象的更“聪明”,但也因此更容易出错。
