刷新页面背后的惊天秘密!99% 的开发者都用错了 HTTP 缓存
你是不是经常听说“加个 Cache-Control 就行了”?但真要你解释清楚 max-age、s-maxage、must-revalidate、no-cache 到底区别在哪,是不是就懵了?
拆解 2022 年最新版 HTTP 缓存标准 RFC 9111,用最接地气的方式带你吃透缓存的每一处细节!
HTTP 缓存到底是什么?为什么它比你想象中更重要?
HTTP 缓存,说白了就是让你的网页加载更快、服务器压力更小、用户体验更丝滑的秘密武器。它不只是浏览器本地存点东西那么简单——你的请求路径上可能有浏览器缓存、CDN 缓存、代理服务器缓存……这些“中间人”都会参与缓存决策。
而控制这一切的核心,就是那个看似简单实则暗藏玄机的 HTTP 响应头:Cache-Control。
这个头决定了内容能被谁缓存、缓存多久、过期后要不要验证、能不能在出错时兜底等等。一个合理的缓存策略,能让你的网站性能提升 50% 以上;而一个错误的配置,轻则白屏卡顿,重则泄露用户数据!
所以说,HTTP 缓存不是“可有可无”,而是每一位现代 Web 开发者必须掌握的基础内功!
新鲜度,缓存的命门:max-age、Expires 和 s-maxage 究竟怎么算?
缓存的第一大问题就是:这个响应还新吗?能不能直接用?这就引出了“新鲜度”(Freshness)的概念。每次缓存遇到请求,都得判断:我手里的这个缓存,还在保质期内吗?RFC 9111 规定了判断新鲜度的优先级规则,非常清晰:
第一优先级,看响应头里的 Cache-Control: max-age=<秒数>。比如 max-age=3600,意思是这个响应在 3600 秒(1 小时)内都是新鲜的,可以直接用,不用去问服务器。这是最现代、最推荐的方式。
如果没 max-age,那就退而求其次,看 Expires 和 Date 头。Expires 是一个绝对时间点(比如 Expires: Wed, 24 Dec 2025 12:00:00 GMT),Date 是响应生成的时间。缓存会用 Expires 减去 Date 得出有效期。但注意:Expires 是 HTTP/1.0 的老古董,且依赖客户端和服务器时间同步,容易出错,所以现在基本被 max-age 取代。
如果连 Expires 都没有,缓存就只能“猜”了——这就是启发式缓存(heuristic freshness)。比如它看到 Last-Modified 头,就可能按“上次修改到现在时间的 10%”来估算有效期。这种行为不可控,强烈不推荐依赖!
特别注意:对于共享缓存(比如 CDN、反向代理),还有一个专属指令 s-maxage=<秒数>。它的优先级高于 max-age,且只对共享缓存生效。这意味着你可以给 CDN 设 1 小时缓存,而给浏览器设 10 分钟,实现精细化控制!
过期了怎么办?条件请求与 ETag 让验证又快又省
缓存过期(stale)不代表就废了!HTTP 的聪明之处在于:它允许你“先验证,再使用”。这就是所谓的“条件请求”(conditional request)。当缓存发现内容已过期,它不会直接丢掉,而是带着“凭证”去问服务器:“我手里的这个版本,还是最新的吗?”
这个“凭证”通常有两种:
一种是 If-Modified-Since: <上次修改时间>,对应服务器曾返回的 Last-Modified 头。但 Last-Modified 精度只有秒级,且文件修改但内容不变时也会触发更新,不够智能。
另一种更强大、更推荐的是 ETag(实体标签)。服务器在响应中附上 ETag: "abc123",这通常是一个基于文件内容、大小、修改时间等生成的哈希值。下次验证时,浏览器就发 If-None-Match: "abc123"。服务器一比对:如果 ETag 没变,说明内容没变,直接返回 304 Not Modified,不用传整个文件!省流量、省时间、省服务器资源!
而且注意:If-None-Match 的优先级高于 If-Modified-Since。所以只要用了 ETag,Last-Modified 基本就退休了。这也是为什么现代 Web 应用几乎都用 ETag 作为缓存验证的核心机制。
下面代码来自JiveJdon经过多个AI打磨和jdon.com实战后的etag代码:
public static boolean checkHeaderCache(long maxAgeSeconds, long modelLastModifiedDate, HttpServletRequest request, HttpServletResponse response) { |
这段代码在:https://github.com/banq/jivejdon/blob/master/src/main/java/com/jdon/jivejdon/util/ToolsUtil.java
Cache-Control 响应指令全解析:max-age、no-store、must-revalidate 到底怎么用?
现在进入重头戏——Cache-Control 响应指令的逐个击破!记住,这些是你在服务器端设置的,用来告诉缓存“你该怎么对我”。
max-age=<秒数>:定义新鲜度生命周期。比如 max-age=86400 表示缓存 24 小时。这是最常用、最基础的指令。
must-revalidate:强调“过期了必须验证”。哪怕网络断了、服务器 5xx 报错,也不能擅自用旧内容。适合金融、支付等对一致性要求极高的场景。
no-cache:名字极具误导性!它不是“别缓存”,而是“用之前必须验证”。效果等价于 max-age=0, must-revalidate。适合那些内容常变,但又想利用 304 节省带宽的接口。
no-store:这才是真正的“别缓存”!要求缓存不能把响应存到任何地方(包括内存)。但 RFC 明确警告:这不能保证隐私安全!恶意缓存或中间人攻击照样能窃取数据。不过它确实会影响浏览器的“前进/后退缓存”(bfcache),Chrome 虽然最近放宽了规则,但多数浏览器还是会因此禁用 bfcache。
private:告诉共享缓存(如 CDN):“这是用户私有内容,别存!”比如用户个人资料页。只有浏览器(私有缓存)可以存。
public:恰恰相反,它允许共享缓存存储带有 Authorization 头(即登录态)的响应。这非常危险,除非你明确知道自己在做什么,比如公有 API 的登录响应其实对所有人一样。慎用!
s-maxage=<秒数>:只对共享缓存生效的 max-age。还能自动附带 proxy-revalidate 的效果,即共享缓存过期后必须验证。
proxy-revalidate:只对共享缓存生效的 must-revalidate。和 s-maxage 搭配使用很常见。
no-transform:禁止中间代理“优化”你的内容,比如自动压缩图片、合并 JS。某些 CDN 会干这种事,如果你的资源是二进制敏感(比如 PDF、字体),务必加上它!
stale-while-revalidate=<秒数>:神技!允许在过期后 <秒数> 内继续用旧内容,同时后台偷偷去验证更新。用户无感知,性能飙升。比如 stale-while-revalidate=60,意味着过期后 1 分钟内还能用老内容,同时触发后台刷新。
stale-if-error=<秒数>:容灾兜底!当验证失败(服务器 5xx、网络断)时,允许在过期后 <秒数> 内继续用旧内容。但注意:浏览器支持度一般,慎用。
浏览器刷新背后的秘密:软刷新 vs 硬刷新如何影响缓存?
你天天点的“刷新”按钮,其实大有讲究!不同浏览器、不同刷新方式,对缓存的处理天差地别。
软刷新(Cmd+R / Ctrl+R):目的是“看看有没有新内容”。Chrome 和 Firefox 会只对主 HTML 文件发条件请求(带 If-None-Match 或 Cache-Control: max-age=0),而 CSS/JS 等子资源仍按原有缓存策略加载——这意味着如果你没改 HTML,CSS 就不会重新验证!Safari 则激进些,直接对 HTML 发无条件请求,不走缓存。
硬刷新(Cmd+Shift+R / Ctrl+Shift+R):目的是“彻底重来”,比如页面加载坏了。三大浏览器此时都会对所有资源(HTML、CSS、JS)发 Cache-Control: no-cache 的请求,强制验证,相当于“清缓存重开”。
有趣的是,Safari 在你硬刷一次后,后续软刷也会带上 no-cache——这可能是 bug,但无伤大雅。
immutable:一个差点改变世界的指令,如今却成“鸡肋”?
时间回到 2015 年,Facebook 发现用户每次刷新动态,浏览器都会对那些永不变的 JS/CSS 文件发一堆 304 请求,纯属浪费!于是 Mozilla 的 Patrick McManus 提出了 immutable 指令:只要响应还在 max-age 有效期内,刷新页面时就别验证了!Facebook 用后效果显著。
但剧情反转:Chrome 没采纳 immutable,而是直接改了刷新策略——软刷新只验证 HTML,子资源照常缓存。Safari 和 Firefox 后来也跟进。结果就是:immutable 虽被写入 RFC 8246,但 Chrome 至今不积极支持,认为“没必要”。
所以今天,除非你还在用老版 Firefox,否则 immutable 的价值已经大打折扣。
认证请求也能被 CDN 缓存?public、s-maxage 和 must-revalidate 的特殊能力
很多人不知道:带 Authorization 头的请求(即登录态请求),默认是禁止被共享缓存(如 CDN)存储的——毕竟用户 A 的数据不能给用户 B 看!但 RFC 9111 留了个后门:如果你在响应中明确加上 public、s-maxage 或 must-revalidate,就等于对共享缓存说:“虽然带登录态,但这个响应其实是公共的,你可以缓存。”这适用于某些特殊场景,比如“所有用户看到的公告内容都一样”。但切记:一旦误用,会导致严重的数据泄露!加之前务必三思!
总结:HTTP 缓存不是配置,而是一套策略思维
HTTP 缓存远不止是几个头字段的堆砌,它是一套精密的协作机制,涉及客户端、中间缓存、源站三方。现代 Web 开发中,合理的缓存设计能带来:
- 用户端:秒开体验,减少等待
- 服务器端:降低 50%+ 流量压力
- 运维侧:提升系统弹性和容灾能力
但错误配置也会引发:内容不更新、用户看到旧数据、甚至隐私泄露。因此,务必做到:
- 静态资源(JS/CSS/图片):用 long max-age + 内容哈希文件名 + immutable(可选)
- 动态 HTML:用 short max-age 或 no-cache + ETag
- 敏感数据:加 private 或 no-store
- 登录接口:谨慎使用 public,避免 CDN 缓存用户私有数据