缓存省下的到底是什么
一个页面打开要几十个请求,JS、CSS、图片全算上。每次都从服务器拉,延迟加上带宽,慢得没法看。缓存干的事就是把上次拿到的文件留在本地,下次直接用。
代价是会出现一种情况:服务器上文件早换了,用户浏览器里还是旧的。所以缓存从来不是开或关的问题,而是允许旧到什么程度的权衡。
麻烦的地方在于,浏览器不是唯一的缓存点。中间还可能有 CDN、公司代理、Service Worker。你清了浏览器缓存还是旧页面,八成是别的层在拦。这一点后面单独讲。
强缓存:直接用,不问服务器
强缓存命中时,浏览器根本不发请求。打开开发者工具,Network 面板里这类请求的 Size 一栏会写着 memory cache 或者 disk cache,时间几乎为零。
控制它的是响应头里的两个字段,Cache-Control 是现代写法,Expires 是老的。
| 响应头 | 含义 | 建议 |
|---|---|---|
| Cache-Control: max-age=3600 | 一小时内不再询问服务器 | 静态资源常用 |
| Cache-Control: no-cache | 可以存,但每次用前必须确认 | HTML 入口常用 |
| Cache-Control: no-store | 完全不许存 | 含敏感信息的响应 |
| Cache-Control: immutable | 有效期内绝不重新验证 | 配哈希文件名用 |
| Expires: 日期 | 老写法,绝对时间,受本机时钟影响 | 给老客户端兜底 |
这里有个特别容易搞混的点:no-cache 不是不缓存。它的意思是缓存可以存,但每次使用之前要跟服务器确认一下。真正完全不存的是 no-store。很多人看到抓包里出现 304 还以为是配置没生效,其实那正是 no-cache 的正常表现。
协商缓存:问一句,没变就省流量
强缓存过期之后,或者响应头要求先确认,浏览器就发一个请求带上验证信息。服务器判断内容没变,回一个 304,不带正文,只带几个头。省的是带宽,省不了往返时间。
# 协商缓存的两种验证方式
# 方式一:ETag(内容指纹,更精确)
请求 If-None-Match: a1b2c3
响应 304 Not Modified
响应 ETag: a1b2c3
# 方式二:Last-Modified(修改时间,精度到秒)
请求 If-Modified-Since: Wed, 15 Oct 2026 08:00:00 GMT
响应 304 Not Modified
响应 Last-Modified: Wed, 15 Oct 2026 08:00:00 GMT
# 内容没变 -> 304,浏览器用本地副本
# 内容变了 -> 200 + 新内容 + 新 ETag
ETag 比 Last-Modified 准。因为修改时间只精确到秒,如果同一秒内改动了两次,或者文件内容变了但时间没变(某些构建工具会这样),Last-Modified 就会误判成没变。所以生产环境最好两个都给,浏览器优先用 ETag。
一次请求的决策顺序
把这些串起来,浏览器面对一个 URL 时其实是按顺序判断的。
有没有缓存副本?
没有 -> 发请求,走正常流程
有 -> 看强缓存是否还在有效期
在 -> 直接用本地副本,不发请求(200 from cache)
过期 -> 带上 ETag / Last-Modified 发请求
服务器说没变 -> 304,用本地副本
服务器说变了 -> 200,下载新内容并更新缓存
看懂这棵树,你就知道自己遇到的属于哪一环。强缓存期太长,用户会长时间看不到更新;协商缓存配得好,用户每次都能拿到最新版但省下带宽。
清了缓存还是旧页面的几种原因
这是最让人抓狂的场景。按下面顺序查,基本能定位。
- CDN 或云加速缓存:源站文件改了,CDN 边缘节点还留着旧副本。这时候清浏览器缓存毫无作用,得去 CDN 控制台刷新缓存,或者等它自己过期。
- Service Worker 拦截:PWA 应用常见。它有自己的缓存策略,优先级高于 HTTP 缓存。开发者工具 Application 面板里能看到,勾上 Update on reload 或者手动 Unregister 才能绕开。
- 公司代理或安全网关:有些内网环境会做内容缓存,尤其是对大体积静态资源。这种情况在自己电脑上怎么折腾都没用,得换网络验证。
- 多标签页:老标签页里跑着旧版 JS,新开一个标签页看到的却是新的,于是误以为页面刷新没生效。把标签页全关掉重开就能确认。
- DNS 或负载均衡没同步:多台服务器部署,其中一台还没更新,请求被轮询到旧机器上。表现是时好时坏,刷新几次可能就好了。
- 其实是构建产物没更新:最讽刺的一种。改了源码但没重新构建,部署的还是旧产物。先登录服务器确认文件内容真的变了,再怀疑缓存。
这个清单里有个共同点:从下往上查。先确认服务器上的东西真的换了,再去追究缓存。顺序反了会浪费大量时间。
开发阶段怎么避开缓存干扰
本地开发时缓存纯属添乱,几种做法按方便程度排。
最快的办法是开发者工具 Network 面板勾上 Disable cache。这个勾选在工具打开期间持续生效,关掉工具就失效,不影响正常浏览。
想强制单次绕过,用 Ctrl+F5(Mac 上是 Cmd+Shift+R)。它会带上 no-cache 请求头,并且跳过本地副本。
查问题时善用 curl,它默认不缓存,看到的永远是服务器真实响应。
# 看响应头里的缓存配置
curl -I https://example.com/app.js
# 带上 If-None-Match 验证 ETag 是否生效
curl -I -H "If-None-Match: a1b2c3" https://example.com/app.js
# 模拟 Ctrl+F5 的请求
curl -I -H "Cache-Control: no-cache" https://example.com/app.js
线上怎么让用户及时拿到新版
核心思路只有一条:入口文件不缓存,静态资源用长缓存加文件名哈希。
HTML 设成 no-cache 或者很短的 max-age,保证用户每次都能拿到最新的入口。入口里引用的 JS、CSS 文件名带内容哈希,比如 app.a1b2c3.js。内容一变文件名就变,旧缓存自然失效,不用手动清任何东西。这样静态资源可以放心设成一年的缓存,既快又不会出问题。
发布后有 CDN 的记得主动刷新,尤其是 HTML 和接口响应。刷新 API 通常有调用次数限制,优先刷入口文件,静态资源靠哈希就够。
常见问题 FAQ
为什么按 Ctrl+F5 就正常了?
Ctrl+F5 是强制重新验证:浏览器会跳过本地缓存,直接向服务器要一份新的,并且带上 Cache-Control: no-cache 请求头。普通 F5 只是普通刷新,浏览器可能仍然用缓存里的副本。所以在开发阶段改完样式看不到变化时,先试强制刷新,能确认问题是不是缓存造成的。
服务端明明改了,用户还是看到旧页面,怎么排查?
按层查:先确认服务器上的文件确实变了(直接登录看文件内容或修改时间);再看响应头里的 Cache-Control 和 ETag 是不是允许长期缓存;然后确认 CDN 有没有缓存住旧版本,需要主动刷新;最后查是不是有 Service Worker 在拦截请求。跳过任何一层,都会出现改了没生效的假象。
Cache-Control 设成 no-cache 是不是就完全不缓存了?
不是。no-cache 的意思是每次使用前必须向服务器确认,服务器说没变就继续用本地副本,仍然省流量。真正完全不存的是 no-store。很多人把这两个搞混,以为设了 no-cache 就一定每次重新下载,实际会看到 304 响应,就是这个原因。
给静态文件名加哈希值有什么用?
文件名一变,浏览器的缓存就自然失效了,等于绕开整个缓存判断流程。常见做法是把 app.js 改成 app.a1b2c3.js,同时把这个文件的缓存时间设得极长,比如一年。文件内容不变就不换名,变了一定时换名,这样既拿到了长期缓存的好处,又不会出现用户拿到旧文件的问题。HTML 入口文件则相反,要设成不缓存或短缓存。