K 的一隅

浏览器 浏览器核心机制

我明明改了文件,浏览器为什么还在显示旧版本:浏览器缓存

从仓库、门店和配送车各自留着旧箱子说起,理解 HTTP 缓存、ETag、304、Service Worker 与旧 JS 部署。

4 分钟阅读 更新于 2026-07-24
本文目录

仓库今天换了新包装,门店却还在卖昨天的货;配送车也可能带着旧箱子回来。你在项目里改了一个 CSS 或 JS 文件,浏览器仍显示旧版本,通常不是编辑器没有保存,而是不同层的缓存各自认为“手里的版本还可以用”。

先猜结果:刷新一定会重新请求吗

js
fetch('/app.js').then((response) => {
  console.log(response.status, response.headers.get('etag'))
})

你刷新页面时,浏览器可能直接使用 memory cache(内存缓存),也可能从 disk cache(磁盘缓存)取文件,也可能带着条件请求去问服务器。如果服务器回答 304 Not Modified,网络上看似有一次请求,实际正文仍然使用本地副本。刷新不是“丢掉所有缓存”的标准按钮。

Cache-Control 先决定能不能直接用

服务器通过 Cache-Control 告诉浏览器响应可以如何保存和复用:

http
Cache-Control: max-age=31536000, immutable

这类资源在一段时间内可以直接使用,不必每次询问服务器。适合带内容 hash 的静态文件,例如 app.8fd2.js;如果 URL 不变却频繁修改,长时间强缓存就会把旧版本留在用户手里。

另一种响应可能允许缓存但要求重新验证:

http
Cache-Control: max-age=0, must-revalidate

“缓存”不等于“永远不问服务器”。缓存策略表达的是新鲜度和验证方式,实际结果还受请求头、代理、浏览器策略和响应是否允许存储影响。

ETag 和 304 是怎么配合的

304 省下的是响应体传输,不代表浏览器完全没有发请求。

服务器可以给资源一个 ETag(实体标签):

http
ETag: "build-42"

下一次需要验证时,浏览器带上:

http
If-None-Match: "build-42"

如果内容没变,服务器返回 304 Not Modified,不重复发送完整正文;如果变了,返回新的正文和新的 ETag。304 不是“请求失败”,也不是“没有响应”,而是“你手里的缓存副本仍可使用”。

手动点击刷新有时会改变验证行为,但开发者工具的 Disable cache 只在工具打开、当前调试上下文中生效。用户真实访问路径仍由响应头决定。排查旧文件时,应同时看 Network 的 request headers、response headers、size 和 from disk/memory cache 信息。

资源 hash 为什么重要

现代构建常把内容摘要放进文件名:app.8fd2.js。代码变了,URL 也变,浏览器不会把旧 URL 当成新文件。HTML 通常设置得更短命,负责指向最新的 hash 文件。

如果 HTML 被长时间强缓存,用户仍可能拿到旧入口;如果 HTML 是新的却引用旧 JS,可能是部署目录、Service Worker 或 CDN 仍在提供旧文件。缓存问题经常是“哪一层保留了旧指针”,而不是简单把所有缓存清掉。

浏览器缓存、CDN 和 Service Worker 不是一个柜子

浏览器 HTTP cache 依据响应头保存资源;CDN(内容分发网络)在服务器和用户之间保存副本;Service Worker 可以主动拦截请求并从 Cache Storage 返回结果:

js
self.addEventListener('fetch', (event) => {
  event.respondWith(caches.match(event.request).then((cached) => {
    return cached ?? fetch(event.request)
  }))
})

这段策略可能让 Service Worker 返回旧响应,即使 Network 看起来请求成功。更新 Service Worker 还涉及安装、等待、激活和控制页面;清理浏览器 HTTP cache 并不会自动清理 Cache Storage。调试时要在 Application 面板分别检查 Service Workers、Cache Storage 和 Storage。

“强制刷新解决了”只能说明当前浏览器绕过了部分缓存,不说明线上用户、手机 WebView 或 CDN 已经拿到新版本。发布策略应该让版本更新可被 URL、HTML 或 Service Worker 的生命周期明确感知。

旧 JS 为什么危险

HTML 和 JS 不是一个原子包。用户可能在部署切换前打开了旧 HTML,切换后刷新拿到新 HTML;也可能新 HTML 引用了 CDN 尚未更新的 JS。若旧 HTML 与新 API 不兼容,页面会白屏或出现“接口不存在”。

常见策略是:静态 JS/CSS 使用内容 hash 和长缓存,HTML 使用较短缓存并谨慎发布;保留一段时间旧静态资源,让旧页面完成更新;Service Worker 更新时明确控制权切换。不要用删除旧文件的方式强迫所有用户立刻升级,长连接和后台标签页都可能仍在使用旧版本。

容易踩的坑

刷新不保证重新下载:可能直接命中缓存,也可能只做条件验证拿回 304(继续用本地正文)。给静态资源加 hash 也挡不住旧 HTML、CDN 或 Service Worker 仍指向旧入口。DevTools 的 Disable cache 只影响当前调试会话,不能代表真实用户不会缓存。