从零星白屏到“启发式缓存”,记录一次刚接手屎山的惊险排查

生产事故排查不仅是硬核技术的体现,也是技术博客里最受欢迎的"流量密码"。深夜完成了一次常规的前端发布,本以为可以睡个好觉。没想到第二天开始,客服渠道陆续收到零星的用户报障:页面一直卡在 Loading 动画,怎么也打不开。

影响面不大,但伤害性极强。作为一个刚接手这个服务、对历史架构还在熟悉的开发来说,这无疑是一场心理与技术的双重考验。


🕵️‍♂️ 第一阶段:神秘的零星白屏与排查盲区

接到通知后,我第一时间尝试在本地和测试环境复现,结果非常完美------一切正常。接着尝试用无痕模式打开,或者让报错用户刷新一次,问题就离奇地消失了。

由于网络链路复杂,ALB(应用负载均衡)反馈报错用户的对应文件响应日志不全,无法通过日志追溯。无奈之下,只能远程拉上故障用户现场抓包。打开 F12 的那一刻,狐狸尾巴终于露出来了:页面请求的几个核心 JS 文件全部报 404

作为一个资深前端,我一眼就看出了端倪:前端构建的静态资源都是带 [hash] 值的。页面在请求旧 hash 的 JS 文件,说明用户手里的入口 HTML 文件是过期的旧版本

但难题才刚刚开始:

  1. 为什么以前的变更从来没出过这问题?
  2. 为什么这次变更我们压根没动网络链路层?
  3. 我第一时间询问了前任维护者,得到的反馈也是"以前从来没遇到过"。

这一切都显得极其诡异。


🧪 第二阶段:严谨的"盲注"与环境复刻

为了排除干扰,我决定在测试环境完全复刻生产环境的 ALB 配置

这里有一个痛点:如果直接发布测试,只要用户一打开 F12 刷新,缓存可能就失效了,错失第一现场。于是我使出了一个破局的小技巧:

💡 破局小技巧:页面埋标记

  1. 先把旧版本软件包发布到测试环境,并在页面 DOM 里偷偷埋下一个微小的版本标记(例如 data-v="old")。
  2. 本地浏览器打开该页面,让浏览器吃下旧版缓存。
  3. 接着在服务器上发布新版本软件包。
  4. 本地浏览器再次访问,直接观察标记是否改变,同时避免了频繁刷新导致的缓存失效。

然而,测试环境依然非常完美地加载了新页面,并没有复现生产的旧 HTML 缓存问题

测试和生产一定有盲区差异!在天天被客服和业务催进度(开发懂的都懂,每天被问"定位出来没有"、"什么时候解决"的窒息感)的压力下,我开始肉眼一行行比对测试和生产环境的 Response Headers(响应头)。

终于,发现了一个致命的不同:生产环境的响应报文中,缺少了 Cache-Control 字段!


🤯 第三阶段:破案!浏览器扮演的"预言家"与 WCM 的背刺

为什么没有 Cache-Control 会导致入口 HTML 被顽固缓存?

通过翻阅 HTTP 规范并借助 AI 辅助确认,我锁定了浏览器一个极其隐蔽的底层机制------启发式缓存(Heuristic Caching)

🧠 技术科普:什么是启发式缓存?

当服务器返回的响应报文中没有任何明确的缓存指令 (没有 Cache-Control,也没有 Expires)时,浏览器并不会任性地每次都去请求服务器。相反,它会扮演"预言家",根据 LM-Factor 算法 自己算出一个缓存时间:

缓存有效时间=(Date(当前收到响应的时间)−Last\-Modified(文件的最后修改时间))×10%缓存有效时间 = (Date (当前收到响应的时间) - Last\-Modified (文件的最后修改时间)) \times 10\% 缓存有效时间=(Date(当前收到响应的时间)−Last\-Modified(文件的最后修改时间))×10%

这与我们观察到的现象高度匹配!因为 HTML 文件可能很久没动过,通过这个公式算出来的缓存时间可能长达数天甚至一个月!这就解释了为什么用户会一直请求旧的 JS Hash,且很难复现(因为一旦由于某些原因触发了强刷,缓存就会被更新)。

那么,为什么生产环境会漏掉这个响应头?

我们顺藤摸瓜,把链路上的 ALB 团队和 WCM(Web内容管理)团队全拉到一个群里对齐。

  • WCM 团队一开始坚称:"我们测试和生产默认都会加上缓存控制响应头。"
  • 现场打脸:"来看生产的真实抓包,确实没有。"

WCM 团队回去排查源码,最终真相大白,令人啼笑皆非(WTF):WCM 系统在近期做了一次更新,它们只对默认的 index.html 自动加上缓存控制,而如果是其他名字的入口 HTML 文件,一律不加!

而我们出问题的那个项目,恰恰不是默认的 index.html


🏗️ 第四阶段:架构复盘,屎山是如何形成的?

看到这里你可能会问:为什么你们前端项目的入口文件不是 index.html?为什么以前没踩坑,偏偏这次踩了?

这就不得不聊聊接手过来的这堆"精妙"的屎山架构了:

  1. Vue CLI 多页应用 (MPA) 架构 :前人使用了 MPA 架构,这导致构建出来的产物包含多个入口文件(如 subpage.html)。这就完美避开了 WCM 系统的 index.html 默认缓存策略。
  2. 多网站共享同一个部署单元(恐怖的路由指向) :本次变更其实根本没有改动这个出服务的网站代码。但是!因为在架构设计上,多个不同的网站在同一套代码里,共享同一个部署单元。这就意味着,多个网站的路由都指向这个地址。你只要更新了 A 网站,其余 3 个网站也会被迫一起重新打包发布。
  3. WCM 变更的滞后性 :兄弟团队的这个 WCM 变动其实已经上线好几个月了,但因为过去这几个月里,我们这个 MPA 项目一直没有触发被迫发布,所以它的 Last-Modified 时间很旧,启发式缓存时间极长。直到这次因为隔壁网站发布,连带导致这个项目重新生成了 HTML,它的 Last-Modified 变成了发布当晚,LM-Factor 算法计算出的缓存时间变短了,这才让"零星用户报错"的现象暴露出来。

🩺 止血与规避方案

这个问题的恶心之处在于,对于已经被启发式缓存坑了的用户,前端是没有任何办法通过代码去帮他刷新浏览器的(因为浏览器压根不向服务器发请求,直接读了本地强存)。

我们最终的对应方案是:

  1. 整改升级:明确责任,将问题升级给 WCM 团队进行紧急配置整改,让他们修复"非 index.html 后缀文件"的缓存头缺失问题。
  2. 长远架构解耦:后续必须将这个多页应用从共享部署单元中剥离出来,做到真正的按需发布,从根源上断绝这种"城门失火,殃及池鱼"的连带发布风险。

📝 总结

这次排查虽然被催得头大,但收获也是满满的。作为高级前端,不仅要对业务代码了如指掌,网络协议层(HTTP 缓存机制)、浏览器底层的边界行为(启发式缓存),以及链路层(ALB/WCM)的运作机制,都是我们手里的武器。

永远不要轻信"以前都没问题"这句话,在分布式和复杂的共享部署时代,往往一个隐藏的底层改动,就能引爆你刚接手的整座屎山。


欢迎在评论区聊聊,你们在生产环境里被哪些隐蔽的"底层缓存机制"背刺过?

相关推荐
程序员黑豆1 小时前
鸿蒙应用开发:6种图片加载方式详解
前端·华为·harmonyos
半个落月1 小时前
用 React 搭一个 WebGPU 模型加载页:从状态驱动到可复用进度条
前端·react.js
雪隐2 小时前
个人电脑玩AI-13让5060 Ti给你打工——我用 0.9B 小模型终结了"谁来记会议纪要"这个世纪难题
前端·人工智能·后端
橘子星2 小时前
我一个前端切图仔,凭什么能在浏览器里跑大模型?
前端·javascript·前端框架
70asunflower2 小时前
初学者理解 Web 工作原理(完全教程)
前端
爱勇宝2 小时前
《道德经》第 7 章:真正厉害的领导者,不抢主角
前端·后端·程序员
观远数据2 小时前
Excel到数据资产池:文件数据入湖的治理规范怎么建
前端·javascript·excel
CoderWeen2 小时前
我写了个能一步步点着看的 Dijkstra 可视化项目(Vue3 + Leaflet + Generator)
前端·javascript·vue.js
两点王爷2 小时前
一个快速加载面和多面数据的html测试页面(可以导出geojson数据文件)
前端·html·gis