
1. 项目概述1.1 从一个让人头疼的场景说起做 Web 开发的人几乎都遇到过这种状况线上报了一个 bug你本地改好代码、刷新页面结果页面还是旧的。你以为是没部署成功反复确认了三四次最后才恍然大悟——浏览器把旧资源缓存住了。你按了 CtrlF5 强制刷新好了页面正常了。但用户那边呢用户不会按 CtrlF5用户只会觉得你改了个寂寞。这就是浏览器缓存最让人又爱又恨的地方。爱它是因为它能大幅减少网络请求、加快页面加载、降低服务器压力恨它是因为一旦缓存策略设计得不好或者根本没有设计它就会变成“你改了代码但用户看不到变化”的背锅侠。这篇内容要解决的问题就是“控制浏览器是否缓存网页状态”这件事——网页里的静态资源、接口返回值、页面本身到底哪些该缓存、哪些不该缓存、缓存多久、怎么强制不缓存、怎么在开发调试时一键禁用缓存以及在真实项目中如何设计一套靠谱的缓存策略。适用范围从纯前端开发者到后端接口提供方、运维、测试都覆盖。如果你只是普通用户想搞明白“为什么网页老是显示旧内容”“怎么彻底清一次缓存”这篇文章也有很大的参考价值。我尽量把方案讲得可直接落地你不需要看第二篇就能自己在项目里实施。1.2 控制缓存的本质是什么所谓“控制浏览器是否缓存网页状态”落实到技术层面其实就是两件事一是告诉浏览器这个资源可以缓存多久、是否可以缓存二是告诉浏览器拿到缓存之后怎么判断这个缓存还有没有效。前者主要由 HTTP 响应头里的Cache-Control、Expires字段来决定后者则由ETag、Last-Modified这类条件请求字段来决定。前端开发者还可以通过meta标签来尝试控制浏览器行为不过这个方法在真实项目中说服力一直有限后面我会单独说。说白了“是否缓存”不是浏览器自己拍脑袋决定的而是服务器通过响应头给浏览器下发的“指令”。只不过很多项目从来没认真设置过这些响应头导致浏览器用自己的默认策略来猜猜出来的结果自然不可控。2. 缓存机制的核心拆解浏览器到底在缓存什么2.1 按资源类型拆开看浏览器缓存不是一个单一的黑盒它其实分了好几个层级和类型。理解这一点才能理解为什么有些东西怎么清都清不掉有些东西明明清了却还是老样子。先按资源类型来看静态资源JS、CSS、图片、字体、视频这类文件。这类资源的典型特点是内容很少变化适合缓存而且适合长缓存。页面文档HTML 页面本身。这类资源的内容变化频率取决于业务但通常不适合长缓存因为 HTML 一旦被缓存它引用的其他资源再多也白搭。接口响应AJAX/Fetch 请求拿到的 JSON 数据。这类缓存以前不太被重视但 [HTTP 缓存设计]做得好的团队会把“接口缓存”当作性能优化的一个重要手段。Cookie / StoragelocalStorage、sessionStorage、IndexedDB这类浏览器本地存储。它们不是 HTTP 缓存但经常被归到“浏览器缓存网页状态”这个大类里调试时也经常要一起清。前三种是 HTTP 缓存管理范畴最后一种是 Web Storage 范畴两者经常被混为一谈但机制完全不同。HTTP 缓存能通过响应头来控制Storage 只能通过代码或开发者工具的清空按钮来处理。2.2 浏览器缓存的存储层级除了按类型分还可以按存储位置分。这是理解“为什么清不掉缓存”的关键。现代浏览器通常有**内存缓存Memory Cache和磁盘缓存Disk Cache**两层。比如 Chrome DevTools 的 Network 面板里Size列会显示from memory cache或from disk cache。内存缓存的读取速度极快但生命周期极短关闭标签页、浏览器进程被系统回收后就没了。磁盘缓存可以持久化保存但读取速度和内存缓存完全不在一个量级。这就解释了为什么有时候你明明清空了缓存刷新一次页面发现请求还是被缓存命中——因为那次命中走的是内存缓存而清缓存操作可能只清了磁盘缓存或者当前页面还持有老资源的内存快照。这也是开发调试时最让人迷惑的一个问题。2.3 浏览器默认缓存行为一个很常见的问题是服务器什么都没设置浏览器就不缓存了吗不是。浏览器会启动启发式缓存Heuristic Caching。如果响应里没有Cache-Control也没有Expires但响应时间在最近 10% 的生存期内浏览器会基于Last-Modified与响应时间的差值按 10% 的比例估算一个缓存时间。举个具体例子服务器返回一个资源Last-Modified是 10 天前浏览器和服务器的时间差是 1 天那浏览器可能就按大约 0.1 天的时长缓存这个资源。这个猜测机制在不同浏览器上表现略有差异但结论是一致的——不做控制浏览器就会乱猜乱猜就会出问题。3. HTTP 协议层的缓存控制手段3.1 Cache-Control 是唯一真相HTTP 缓存控制的现代方案核心就是Cache-Control响应头。它由一组指令组成用逗号分隔常用的有这些指令含义典型用法no-cache缓存前必须先向服务器验证不能直接使用适合 HTML、动态接口no-store禁止任何形式的缓存直存适合登录态、支付接口、隐私数据public允许客户端和中间代理缓存适合静态资源 CDNprivate只允许浏览器缓存不允许中间代理缓存适合带用户信息的页面max-age秒缓存有效时长max-age31536000常用于带指纹的静态资源must-revalidate缓存过期后必须回到服务器验证配合max-age使用这些指令可以组合但要注意语义不能冲突比如max-age和s-maxage同时出现时要搞清楚哪个给浏览器用、哪个给 CDN 用。s-maxage是专门给共享缓存比如 CDN用的浏览器会忽略它。3.2 强缓存与协商缓存缓存要不要回服务器验证是理解 HTTP 缓存的一根主线。业界把缓存分成两类强缓存在有效期内直接用本地副本完全不发请求。对应Cache-Control: max-agexxxx或Expires字段。只要没过期浏览器连请求都不会发出Network 面板显示200 (from disk cache)或200 (from memory cache)。协商缓存本地有副本但能不能用要问一下服务器。对应ETag和Last-Modified。浏览器会带着这些标识去问服务器——如果服务器说“没变”返回304 Not Modified浏览器接着用本地副本如果服务器说“变了”返回新的200浏览器用新资源并重新缓存。这两种缓存的组合策略基本决定了网页个体资源的加载速度。静态资源一般走强缓存HTML 文档和接口通常走协商缓存或不缓存这是业界最普遍的做法。3.3 ETag 和 Last-ModifiedLast-Modified是服务器返回的资源最后修改时间浏览器第二次请求时带上If-Modified-Since服务器对比时间即可。实现简单但精度只有秒级如果资源在 1 秒内改了好几次它就感知不到了。ETag是服务器返回的资源标识符通常是内容哈希值。浏览器第二次请求时带上If-None-Match服务器比对标识。如果内容变了哈希自然就变了。它比Last-Modified更可靠但服务器算哈希也需要一点开销不过这个开销通常可以忽略。现在的主流实践是两者同时返回服务器优先校验ETag。这也是 CDN 的默认行为大部分框架如 Nginx、Spring Boot、Express都默认支持这两个字段只是很多人从来不知道它们帮自己扛了多少事。4. 代码层面的缓存控制实操4.1 HTTP 响应头设置不同后端技术栈设置方式不一样但核心思路一致。以常见的几种后端为例Nginx 配置静态资源缓存location /static/ { expires 30d; add_header Cache-Control public, max-age2592000, immutable; } location /index.html { add_header Cache-Control no-cache, no-store, must-revalidate; }immutable是一个容易被忽略的指令它告诉浏览器这个资源在失效期内完全不用再发请求验证。Chrome 在地址栏回车时对含immutable的资源即使max-age未过期也不会重新验证主动刷新才会。Spring BootJavaConfiguration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/static/**) .addResourceLocations(classpath:/static/) .setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS) .cachePublic().immutable()); } }ExpressNode.jsapp.use(/static, express.static(public, { maxAge: 30d, immutable: true, etag: true }));我实际测试下来这三个方案的最终效果完全一致区别只在于团队技术栈不同。选型的时候不要纠结“哪个方案最牛”应该选你们团队能维护的那一个这一点后面设计缓存策略的时候会重点说。4.2 前端 meta 标签控制前端想通过 HTML 来控制缓存常用写法是这样的meta http-equivCache-Control contentno-cache, no-store, must-revalidate meta http-equivPragma contentno-cache meta http-equivExpires content0http-equiv原本是设计来模拟 HTTP 响应头的但现代浏览器对meta方式的支持已经很不一致了。Chrome 对Cache-Controlmeta 标签基本无视Firefox 部分场景有效而且当服务器响应头存在时响应头的优先级更高。所以我的建议是meta 标签可以写但不能作为唯一方案。它真正的价值在于拦截一些代理服务器或弱网环境下的历史缓存行为尤其是Pragma: no-cache对某些中间缓存设备还有一定约束力。真正要控制浏览器缓存必须在服务端响应头上下手。4.3 动态接口的缓存控制接口要不要缓存、如何缓存得按业务来定。低风险做法是公开数据走短缓存用户私有数据坚决不缓存。如果接口返回的数据是全局通用的比如配置文件、公共字典、商品列表可以在响应里加上Cache-Control: public, max-age60这样一分钟内用户重复打开页面接口请求直接命中浏览器缓存连请求都不发出去。但要小心——某些团队在“接口缓存”上翻车不是因为缓存本身有问题而是因为没加Vary头。比如同一个 URL 根据Accept: application/json或Accept: text/html返回不同内容浏览器缓存了 JSON 响应下次请求 HTML 时直接返回 JSON页面就崩了。这种情况一定要加Vary: Accept让浏览器对不同请求头的响应分开缓存。用户维度相关的接口最安全的响应头是这个Cache-Control: no-store不要用no-cache因为no-cache只是“先验证再用”它仍然会把响应体存下来。涉及银行卡、收货地址、订单列表这类数据时no-store才是真正的“一点都不存”。很多团队在这里搞混拿no-cache当“不缓存”用结果数据敏感度一高就出问题。4.4 Service Worker 手动控制HTTP 缓存是浏览器自动行为但如果你需要更细粒度的控制——比如“这个接口在断网情况下使用缓存兜底”“那个页面必须走缓存不能请求服务器”——那就得请出 Service Worker。Service Worker 里有一个专门的缓存 API叫 [Cache Storage]和浏览器 HTTP 缓存是两套独立系统。你可以这样写// 安装阶段预缓存关键资源 self.addEventListener(install, (event) { event.waitUntil( caches.open(app-shell-v1).then((cache) { return cache.addAll([/, /index.html, /static/main.js]); }) ); }); // 请求阶段拦截策略 self.addEventListener(fetch, (event) { event.respondWith( caches.match(event.request).then((cached) { const network fetch(event.request).then((response) { if (response response.status 200) { const clone response.clone(); caches.open(dynamic-v1).then((cache) cache.put(event.request, clone)); } return response; }).catch(() cached); return cached || network; }) ); });上述代码实现的是 Cache First 带网络回退的静态资源策略。日常离线包、PWA 应用基本都是这个套路。但注意Service Worker 一旦上线更新它会比较麻烦——因为 Service Worker 本身也是被缓存的。实际操作中我一般给 Service Worker 文件设置Cache-Control: no-cache或者干脆用no-store凡是涉及 SW 文件的响应一律不让浏览器插手缓存避免出现“代码改了但 SW 不更新”的死局。5. 前端开发的强制不缓存手段5.1 DevTools 的 Disable cache 开关Chrome DevTools也就是热搜词里反复出现的谷歌浏览器开发者工具的 Network 面板里有一个Disable cache选项。勾选后只要 DevTools 处于打开状态所有请求默认带上Cache-Control: no-cache和Pragma: no-cache相当于浏览器自动帮你强制验证每次请求。这个开关太好用了但有一个大坑只有 DevTools 处于打开状态时才生效。你关掉 DevTools 再点页面Disable cache 就失效了页面可能又走缓存。我见过不少前端同事低估了这个开关的范围语义以为勾了一次就永久生效结果排查问题排了半天发现“哦原来忘了开 DevTools 或不小心关了”。如果你要长期验证一个页面的真实缓存行为建议有两种做法一是用无痕窗口。无痕模式下默认不会读取浏览器现有的磁盘缓存每次打开都是一个相对干净的缓存环境。但注意无痕模式不是完全无缓存只是关闭时自动清除同一次无痕会话内缓存依然正常工作。二是用 DevTools 的 Network 面板右键选择Clear browser cache。这个操作会把整站的磁盘缓存清掉内存缓存一般也会被清干净。比 CtrlShiftDelete 去设置里清快得多。5.2 强制刷新的几种姿势CtrlF5 / CmdShiftR忽略强缓存强制重新请求资源。但某些情况下如果资源已被内存缓存快照占用这种方式仍可能不触发真实请求遇到就配合 DevTools 的 Disable cache 一起使用。CtrlShiftDelete清掉整站的所有缓存数据、Cookie 和站点数据适合从大扫除开始做调试的情况。地址栏直接回车这个行为比较特殊它对带immutable的静态资源会跳过验证直接使用缓存。这正是immutable指令的设计意图——减少不必要的重复请求。我自己的调试习惯是先从 Network 面板看Size列确认资源到底是从 memory cache、disk cache 还是网络加载的再决定用哪种方式刷。盲目刷新十次不如先看懂那一次的资源来源。5.3 Python Selenium 自动化清缓存测试和爬虫场景里经常要用 Selenium 控制浏览器这时清缓存也有对应的 API。Selenium 老版本用driver.delete_all_cookies()只能清 Cookie对于 HTTP 缓存是无能为力的。现在推荐的做法是使用 Chrome DevTools ProtocolCDP接口直接清空缓存from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() # 禁用磁盘缓存避免测试之间相互污染 options.add_experimental_option(prefs, { profile.default_content_setting_values.notifications: 2, disk-cache-size: 0, }) driver webdriver.Chrome(optionsoptions) # 通过 CDP 清空浏览器缓存 driver.execute_cdp_cmd(Network.clearBrowserCache, {}) driver.execute_cdp_cmd(Network.setCacheDisabled, {cacheDisabled: True})第一行Network.clearBrowserCache是清空现有缓存数据第二行Network.setCacheDisabled是直接让后续所有请求都不走缓存。配合使用就能在自动化测试中得到一个“每次运行都像首次访问”的浏览器环境。这在高频 UI 自动化测试里非常有用能消除大量因为资源缓存导致的随机失败。要注意的一点是execute_cdp_cmd不是 Selenium 官方标准 API它是调用了 Chromium 的底层协议所以在非 Chromium 内核浏览器比如 Firefox 的 GeckoDriver上不适用。Firefox 上可以用set_preference(network.http.use-cache, False)来禁用 HTTP 缓存但功能深度和 Chrome 相比还是有差距的。6. 真实项目中的缓存策略设计6.1 静态资源指纹与长缓存页面发布最大的痛点是JS/CSS 文件名不变但内容已经变了用户还在用旧文件。标准解法是内容指纹。打包工具Webpack、Vite、Rollup在构建时会把文件内容做一个哈希生成类似main.8f3k2d.js的文件名。内容一改哈希就变文件名也变浏览器把它当成全新资源来请求旧缓存自然就失效了。配合这个机制静态资源的 HTTP 缓存头可以放心大胆地拉长Cache-Control: public, max-age31536000, immutable一年不验证也没关系因为文件名本身就是版本号。文件名不变说明内容一定没变用一年前的缓存完全正确文件名变了说明内容更新了浏览器会重新下载。这个方案看似简单但实践中有个容易忽视的前提HTML 一定不能长缓存。如果 HTML 被缓存了 10 分钟那么 10 分钟内发布的新版本资源就算文件名变了用户也拿不到新的 HTML 去引用它。所以 HTML 的响应头必须设置为Cache-Control: no-cache或者干脆用no-store。no-cache意味着浏览器每次都要向服务器验证只要服务器返回 304 就用本地副本no-store则彻底不走缓存。生产环境我倾向前者因为 304 验证的代价很轻同时保留了一个“万一服务器不可用浏览器还能用本地副本凑合看”的降级余地。6.2 版本发布与缓存更新的配合指纹方案能解决文件名变化的问题但如果你的发布流程不支持“文件名变动后仍能部署到正确路径”那指纹方案也会失效。比如有些老旧的发布系统会把所有静态文件按固定文件名部署那么指纹策略就得改成“固定文件名 短缓存时间”的方案。这个方案下资源名永远叫main.js但内容会变。缓存策略就变成Cache-Control: public, max-age600十分钟的强缓存既保证了用户刷新页面时大概率能拿到新内容因为十分钟已经过了又能在短时间内减少重复请求。它没有指纹方案那么优雅但在发布系统不支持文件名变动的场景下已经是成本最低、可操作性最强的方案了。还有一个实践细节CDN 的缓存刷新和浏览器缓存是两回事。CDN 有自己的缓存层你改了源站资源但 CDN 没刷新用户依然拿不到新内容。所以每次发布前除了检查源站响应头还要记得触发 CDN 刷新操作。这一步经常被遗忘线上出现“用户看不到变化”的案例里有相当一部分是 CDN 缓存没刷新而不是浏览器缓存的问题。6.3 用户态页面的缓存策略动态页面、登录后的个人中心、购物车等场景核心原则就一条不要让浏览器帮你缓存用户私有数据。推荐统一设置Cache-Control: no-store很多人会担心no-store影响性能其实没必要。这些页面的性能瓶颈在接口响应时间、数据库查询、渲染耗时这些地方而不是 HTTP 缓存。真正需要性能优化时是在服务端做数据层的缓存比如 Redis 缓存用户购物车结构而不是把响应结果塞给浏览器让它存在本机磁盘上——你想公共电脑上的浏览器如果缓存了某个用户的订单信息下一个人按一下 CtrlH 就能看到这得是多大的安全事故。6.4 跨浏览器差异与兼容热搜词里有个“跨浏览器支持的设计与实现”在缓存领域确实存在这个坑。Chrome、Firefox、Safari、Edge 对 Cache-Control 的指令支持程度并不完全一致。此前我踩过一个真实的坑在 Safari 上Cache-Control: no-store表现得不是那么坚决某些情况下它仍然会把资源写进磁盘缓存。排查问题时最后是给 HTML 响应头额外加了一个Expires: 0Safari 的意外缓存才消失。类似的边缘案例还不少所以在设计缓存策略时不能只盯着 Chrome 验证一次就上线。至少要过一遍 Firefox 和 Safari尤其是涉及no-store、immutable、Service Worker 的场景。对于项目中有明确兼容性要求的团队我的建议是在测试用例里加一条“缓存头验证”用例专门断言关键页面的响应头是否符合预期。这样即使换人维护也不至于把缓存策略悄悄改坏。7. 常见缓存问题的排查实录7.1 页面改完刷不出来这是最高频的问题处理方法按顺序来打开 DevTools - Network看资源的Size列。如果显示from disk cache或from memory cache说明命中了强缓存。点击请求看 Response Headers 里的Cache-Control确认是不是max-age太长。确认 HTML 本身是否设置了no-cache。如果 HTML 被缓存下面所有分析都是白费。确认文件名的指纹有没有变化。如果打包产物没有生成新哈希请先检查构建流程。如果服务器没问题但用户还是反馈旧页面检查有没有 CDN 层把 CDN 的刷新按钮点了。排查这类问题最大的忌讳是“猜”。不要凭感觉说“应该是缓存问题”每一步都看数据很快就能定位到具体那一层。7.2 接口数据异常如果接口数据一直是旧的先看有没有命中from disk cache。网络面板里Size列显示from disk cache的接口是强缓存命中页面代码再新也没用。常规处理是给接口响应加no-store。如果你不敢确定接口是否会被浏览器缓存先在 Network 面板里看响应头如果发现Cache-Control是no-cache但仍有旧数据十有八九是中间代理比如公司网络代理、路由器代理把响应缓存了。此时需要看Via头或X-Cache头判断是哪个代理节点在做缓存。另一种接口异常场景代码中配置了 Service Worker 拦截请求并返回旧缓存。这个问题只在生产环境出现因为本地开发一般没注册 SW。排查时在 Application 面板的 Service Workers 里找到当前页面注册的 SW点 Unregister 即可。我见过非常多的“线上接口是旧的”案例最后查出来是 Service Worker 版本没更新导致的这个坑比 HTTP 缓存隐蔽得多。7.3 清除缓存后仍然生效清缓存是一个常见的“我以为清了其实没清干净”的操作。重点在于区分缓存存储位置内存缓存关掉标签页或浏览器进程即可释放。磁盘缓存DevTools - Application - Storage - Clear site data或者浏览器设置里的清除缓存数据。Service Worker 缓存需要在 Application - Service Workers 里逐个 Unregister再通过caches.keys()逐个删除 Cache Storage。普通的清缓存操作不会自动删除Cache Storage 里的数据。预加载缓存Chrome 的预渲染功能也会生成缓存平时排查时可以临时关掉chrome://flags里的 “Preload pages” 选项。清缓存清不干净多数情况是没意识到 Cache Storage 和 Service Worker 是独立于 HTTP 缓存的另一套体系。7.4 日志与调试经验排查缓存问题DevTools 的 Network 面板是最重要的工具有三个小技巧值得养成习惯第一勾选 Network 面板的Disable cache时注意它会强制所有请求走网络这会掩盖真实环境下的加载性能所以验证线上性能时千万不要开着它。第二把 Network 面板的缓存类型显示列打开一眼就能看出当前请求是 memory cache 还是 disk cache。Chrome 中在 Network 面板列头右键勾选Cache列即可。第三如果怀疑是 CDN 缓存影响可以直接用 curl 带上一组特殊请求头来绕过 CDN 缓存比如自定义一个请求头让 CDN 回源。不同 CDN 厂商的绕过方式不同实际操作时需要去查对应平台的回源测试方法。7.5 适应低版本浏览器的降级方案虽然现代浏览器已经非常规范但在某些系统环境里仍然可能存在低版本浏览器用户。缓存控制的兼容性问题主要集中在低版本 IE 对Cache-Control的支持不完整需要配合Expires: 0和Pragma: no-cache。部分旧版 Safari 对no-store支持不完善需要额外设置Expires。企业内网浏览器可能被统一配置了强制缓存策略前端无能为力只能通过给 URL 加版本号参数来绕过。加版本号参数是值得掌握的兜底手段比如把https://example.com/api/list变成https://example.com/api/list?_t1699999999这样 URL 变了浏览器就认为是新请求。这个方法虽然能解决“客户端缓存旧数据”的问题但不是长久之计只适合紧急修复长期方案仍然是优化响应头策略。8. 开发调试时的缓存控制技巧补充8.1 不同场景下缓存开关怎么选当你在浏览器里调试页面时切换不同场景需要不同的缓存放行策略场景推荐操作原因正在改样式/组件DevTools 勾选 Disable cache每次刷新都拿到最新资源验证生产环境的真实加载关闭 Disable cache保持默认状态模拟真实用户缓存行为排查线上 bug 是否为缓存导致无痕窗口重新访问避开历史缓存干扰自动化回归测试CDP 强制禁用缓存测试结果稳定可复现这个表做好之后能让不同角色开发、测试、运维各自找到对应的操作方式不用每次都靠猜。8.2 利用网络限速测试缓存行为调试缓存时很多人忽略了一个有用的功能——DevTools 的 Network 面板里的Throttling选项。把网络限速为 Slow 3G再去刷新带缓存资源的页面你会发现资源是否走缓存一目了然走缓存的瞬间完成不走的卡半天。这个方法尤其适合演示“为什么静态资源要长缓存”这个道理。给团队做内部分享时我常用这个演示来直观说明缓存能省多少流量和时间比任何理论讲解都管用。另外提一嘴Edge 浏览器本身的缓存行为与 Chrome 基本一致都是 Chromium 内核所以上面所有 DevTools 的操作在 Edge 里同样适用。“Edge 浏览器内存占用高”的问题有一部分就是因为缓存资源太多了内存缓存堆积定期清理站点数据或关掉不必要标签页能缓解这个算是题外话但值得顺带一提。8.3 热更新与开发服务器的缓存处理前端开发默认吃的“热更新”HMR本质上就是开发服务器主动告诉你“这个文件变了”然后浏览器帮你把旧的模块替换掉。它绕过 HTTP 缓存的方式是靠开发服务器在响应里设置Cache-Control: no-store保证每个模块更新都能即时生效。如果你在本地开发时发现“改代码但页面没反应”首先检查开发服务器是不是把 HMR 关了其次是看 DevTools 的 Network 面板里main.js是不是被缓存命中。这个问题比想象中更常见在 Chrome 上尤其如此因为 DevTools 开着时 Disable cache 没勾选开发服务器的no-store又没生效旧模块就一直赖在内存里。9. 从缓存控制到性能优化的延伸9.1 缓存是性能优化的一等公民很多人做性能优化只盯着压缩图片、合并请求、上 CDN 这些“强手段”却忽略了缓存其实是性价比最高的一招。一次缓存命中就能省下整个 HTTP 往返时间尤其是移动端弱网环境客户端缓存命中带来的性能提升比任何压缩算法都直观。一个典型的页面加载时间拆解里静态资源请求往往占比最大。如果这些资源能命中本地缓存加载时间几乎可以归零。所以完整的性能优化方案里缓存策略一定是核心组成。我自己的一个判断标准是如果一个页面首次访问要 3 秒第二次访问要 2.8 秒说明缓存完全没起作用如果第二次访问降到 0.8 秒说明缓存策略生效了。用这个标准去验证自己做的缓存配置到底有没有用比看一堆指标图更直接。9.2 与服务端缓存的边界热搜词里有不少关于“redis 缓存”“缓存设计”的内容这些是服务端缓存主题。服务端缓存和浏览器客户端缓存解决的是不同层面的问题服务端缓存Redis/Memcached减少后端计算和数据库查询。CDN 缓存减少源站压力缩短用户和服务器之间的物理距离。浏览器缓存减少重复网络传输加快页面加载。三层可以同时存在互不冲突。比如一个商品详情页页面本身走 CDN 缓存用户浏览器对静态资源走强缓存接口数据由服务端 Redis 缓存支撑CDN 边缘节点再缓存一层 API 网关结果。每一层都有独立的价值但每层也都有自己的失效机制需要分别管理。线上问题的排查很多时候就是逐层判断“这一层有没有问题”这也是为什么我会反复强调看响应头的原因——响应头里带着每一层的缓存标识看到X-Cache: HIT就是 CDN 命中看到Age: 120就是 CDN 缓存了 120 秒。9.3 缓存策略的安全影响最后必须提一嘴安全。缓存如果控制不好不只是性能问题更是数据安全问题。我之前碰到过一个线上事故某个系统的用户资料接口没设置Cache-Control浏览器默认把它缓存到了磁盘结果公共电脑的下一个用户通过 DevTools 直接把上一个用户的手机号、家庭地址都看全了。这类问题的解决很简单就是统一给所有涉及用户敏感数据的接口设置Cache-Control: no-store同时在页面 HTML 层面加上Pragma: no-cache。根据我的经验与其靠开发者自觉不如在项目的过滤器/中间件层统一加规则凡是/api/user/、/api/order/等路径前缀的请求强制覆写响应头为no-store。从根上让敏感接口根本没有机会被浏览器缓存这才是最稳妥的做法。10. 最后的实操建议与个人心得做了一圈浏览器缓存控制我个人最大的体会是缓存控制不是“加一个响应头”的事而是一整套需要设计和测试的策略。很多项目上线时没想过缓存策略出问题后临时加各种no-cache结果页面性能暴跌用户比之前更不满。所以我的建议是新项目在第一天就把缓存策略定下来老项目抽一个下午专门梳理一遍关键资源、页面、接口的响应头避免将来被各种莫名奇妙的“旧版本”问题反复折磨。如果你现在正准备做这件事我推荐按这个顺序推进先给所有 HTML 文档统一设置Cache-Control: no-cache确保页面本身永远能及时更新。给带内容指纹的静态资源设置Cache-Control: public, max-age31536000, immutable。给用户私有数据接口设置Cache-Control: no-store。在测试环境加一条自动化用例定期检查关键路径的缓存响应头。上线后观察一段时间如果发现还有少数用户出现“旧页面”问题优先检查 CDN 刷新流程和 Service Worker 版本管理。最后再分享一个我自己经常用的“自查小程序”打开目标网站的 Network 面板刷新页面后把所有请求按Size列排序凡是显示from disk cache或from memory cache的都点开看一眼响应头。如果发现一个本不该缓存的接口被缓存了或者一个应该长缓存的静态资源没缓存记录下来该修的修该调的调。这个习惯坚持两三个项目之后你对浏览器缓存的理解会远超绝大多数同行。