本文要点
同样是按刷新,浏览器发出的请求并不相同,这解释了为什么有时刷新一下就变、有时怎么刷都不变
如果服务器返回的响应带了 Cache-Control: max-age,浏览器会按这个秒数判断响应是新鲜还是过期
很多人以为把响应设成 no-store 就能保证每次都看到最新内容,但这两个指令的作用点不同

本文目录
先分清旧内容是从哪一层拿出来的
你更新了服务器上的页面,浏览器里却还是旧样子。要判断问题出在哪,先分清缓存有两类。
按 MDN 的 HTTP 缓存说明,缓存分私有缓存和共享缓存。私有缓存绑定到特定客户端,通常就是浏览器缓存,它存储的响应不与其他客户端共享。共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应,进一步分为代理缓存和托管缓存。
这个区分决定了排查方向。如果只有你这一台设备看到旧内容,别的设备或别的网络下看到的是新内容,那更可能是浏览器这个私有缓存在复用旧响应。如果多台设备、多个网络下都看到旧内容,就要考虑中间的共享缓存,比如代理缓存或你部署的托管缓存。
一个自建情境(示例,不是对某个真实网站的测试):你在练习目录里维护一个自建页面,改完文字后本机打开还是旧的,但用另一台设备访问同一地址看到的是新的。这时优先怀疑本机浏览器缓存,而不是服务器没更新。
适用范围:本文只讲浏览器缓存与中间缓存的区分、刷新行为差异,以及 Cache-Control 的基础含义。不涉及具体浏览器的菜单名称和位置,也不判断搜索引擎是否收录。
普通刷新和强制重新加载发出的请求不一样
同样是按刷新,浏览器发出的请求并不相同,这解释了为什么有时刷新一下就变、有时怎么刷都不变。
按 MDN 的说明,浏览器重新加载时发出的请求里带有 Cache-Control: max-age=0,并带上 If-None-Match 和 If-Modified-Since 做验证。max-age=0 的意思是“只重用存活时间为 0 或更少的响应”,因此中间存储的响应不会被重用。请求会带着验证器去问服务器:这个资源变了没有。
强制重新加载发出的请求不同:它带的是 Pragma: no-cache 和 Cache-Control: no-cache。MDN 指出,由于这不是带 no-cache 的条件请求,你可以确定会从源服务器拿到 200 OK。
所以两种操作的区别可以这样记:普通刷新是“先问服务器有没有变”,服务器说没变就回 304 Not Modified,浏览器继续用本地那份;强制重新加载是“绕过缓存直接要一份新的”。
一个边界:MDN 提到来自 Chrome、Edge 和 Firefox 的请求看起来很像上面描述的,来自 Safari 的请求会有点不同。本文不写各浏览器菜单里这两个操作具体放在哪,因为资料没有给出这些位置。
max-age 决定响应还能被复用多久
如果服务器返回的响应带了 Cache-Control: max-age,浏览器会按这个秒数判断响应是新鲜还是过期。
按 MDN 的说明,存储的 HTTP 响应有两种状态:新鲜和过期。判断标准是存活时间,也就是响应生成以来经过的时间。以 max-age=604800 为例,604800 秒相当于一周:存活时间小于一周,响应是新鲜的;超过一周,响应就过期了。只要响应保持新鲜,它就会被用来兑现客户端请求。
这解释了一个常见现象:你更新了服务器上的文件,但浏览器手里那份还没到 max-age,它就不会去问服务器,直接用旧的。
共享缓存还会把已经存了多久告诉客户端。MDN 的例子是,共享缓存把响应存了一天之后,返回给客户端的响应里会带 Age: 86400。客户端据此算出剩余有效时间,是 max-age 和 Age 的差。
一个自建示例(不是实测数据):假设某个练习响应写了 max-age=3600,你在第 10 分钟改了服务器上的内容,此时浏览器手里那份还在新鲜期内,普通刷新也可能先复用本地响应。要让它去问服务器,需要触发验证,或者等它过期。
no-cache 和 no-store 不是一回事
很多人以为把响应设成 no-store 就能保证每次都看到最新内容,但这两个指令的作用点不同。
按 MDN 的说明,no-cache 不会阻止响应的存储,它阻止的是在没有重新验证的情况下重用响应。也就是说,响应可以被存下来,但每次用之前都要先向服务器验证。
no-store 则是阻止把响应存储在任何缓存中。
MDN 还专门提醒了一个容易踩的点:no-store 阻止存储响应,但不会删除相同 URL 已经存下的响应。换句话说,如果某个 URL 之前已经存了旧响应,后来改成返回 no-store,并不能阻止那份旧响应被继续复用。而 no-cache 会强制客户端在重用任何已存储的响应之前先发验证请求。
所以对“希望始终提供最新内容”这个目标,MDN 给出的方向是用 no-cache,而不是 no-store。资料同时指出,不建议随意使用 no-store,因为会失去 HTTP 和浏览器的一些优势,包括浏览器的后退/前进缓存。
一个边界:MDN 提到 no-cache 与 private 组合,可以在需要个性化的场景下既保证验证、又避免与其他用户共享。
过期不等于作废,验证通过会返回 304
响应过期之后,浏览器不会立刻把它丢掉,而是可以走一次验证。
按 MDN 的说明,验证是通过带 If-Modified-Since 或 If-None-Match 的条件请求完成的。以 If-Modified-Since 为例:客户端带着上次的修改时间去问服务器,如果内容自那个时间以来没有更改,服务器返回 304 Not Modified。这个响应只表示“没有变化”,没有响应主体,只有一个状态码,所以传输量很小。客户端收到后,会把存储的过期响应恢复为有效的,继续复用。
ETag 走的是另一条路。ETag 的值由服务器生成,服务器可以按自己的方式设置,比如用主体内容的散列或版本号。过期后,客户端把缓存的 ETag 放进 If-None-Match 请求头去问服务器;如果服务器算出的 ETag 和请求里的一致,返回 304;如果服务器认为现在应该有不一样的 ETag,就返回 200 OK 和最新版本。
MDN 还提到,如果同时存在 If-Modified-Since 和 If-None-Match,对验证器而言 If-None-Match 具有优先级。
一个自建示例(不是实测记录):你在练习目录里放一个自建页面,服务器给它配了 ETag。你改了内容后,浏览器发条件请求,服务器算出 ETag 变了,于是回 200 和最新内容;如果没改,就回 304,浏览器继续用本地那份。
中间缓存和请求合并会让旧内容看起来更顽固
如果多台设备都看到旧内容,问题可能不在浏览器,而在中间的共享缓存。
按 MDN 的说明,共享缓存主要位于源服务器之前,目的是减少到源服务器的流量。当多个相同的请求同时到达共享缓存时,中间缓存会代表客户端把单个请求转发到源,然后源把结果复用于所有客户端,这称为请求合并。
关键点在这里:MDN 指出,当请求同时到达时会发生请求合并,因此即使响应里给了 max-age=0 或 no-cache,它也会被重用。也就是说,你按刷新发出的请求,可能被中间这一层合并掉了。
MDN 还提到,托管缓存由服务开发人员明确部署,用来降低源服务器负载,例子包括反向代理、CDN,以及 service worker 与缓存 API 的组合。托管缓存的特性因部署的产品而异,多数情况下可以通过 Cache-Control 和配置文件或仪表板控制。
一个边界:MDN 说明,HTTP 缓存规范本质上没有定义显式删除缓存的方法,但托管缓存可以通过仪表板操作、API 调用、重新启动等方式实时删除已存储的响应。这部分具体怎么操作,取决于你用的产品,本文不展开。
按现象缩小范围,再决定下一步查什么
把上面的内容整理成一份自查顺序,帮你从现象出发缩小范围。以下顺序是编辑整理的判断方法,不是官方排查流程。
1. 只有本机看到旧内容,其他设备或网络看到新内容:优先看浏览器这个私有缓存,关注响应的 max-age 和是否走了验证。
2. 多台设备、多个网络都看到旧内容:把中间缓存纳入考虑,包括代理缓存和你部署的托管缓存。
3. 普通刷新不变、强制重新加载变新:说明问题在缓存复用这一层,而不是服务器没更新。因为强制重新加载会绕过缓存直接拿 200。
4. 改了服务器内容但浏览器一直不更新:看响应有没有带 Cache-Control。MDN 指出,即使没有给出 Cache-Control,满足某些条件时响应也会被存储和重用,这叫启发式缓存。资料举的例子是,内容整整一年未更新,客户端会存储该响应并在一段时间内重复使用,重复使用的时间长短取决于具体实现,规范建议约为存储后时间的 10%。MDN 同时说明,原则上所有响应都应显式指定 Cache-Control 标头。
5. 想避免被启发式缓存影响:MDN 给出的常见做法是让默认的 Cache-Control 值包含 no-cache;如果内容是为每个用户个性化的,还要加上 private。
一个边界:本文不给出“看到旧内容就一定是缓存问题”的结论。服务器是否真的更新成功、URL 是否指向了同一个资源,这些不在本文范围内。
观看知识要点视频 18 秒
18秒无旁白要点导览,文字来自已审核正文;完整步骤、条件及参考资料见下方。
参考资料
以下原始资料提供本文依据;请结合日期、适用条件和产品说明查阅。
请结合原始资料的适用条件阅读。
返回浏览器工具 →