Skip to content

我把 “请手动清缓存” 从我的技术词典里删除了

不知道从什么时候开始,“清缓存” 成了前端和用户、产品之间心照不宣的“ 遮羞布”。

以前每逢发版,群里准会出现那几句熟悉的对白:“页面怎么白屏了?” “旧代码逻辑还在?” 而我总是习惯性地回一句:“你强制刷新一下试试。” 虽然问题解决了,但心里总觉得这事做得 不够优雅

最近花时间复盘了前端缓存的底层逻辑,把“发版自愈”这套方案落地后,我才意识到:用户根本不该知道什么是缓存。

🌳 指望浏览器“听话”是不现实的

我以前总觉得配好了 Cache-Control 就万事大吉,但实际情况复杂得多:

  • 入口文件被锁死: 最头疼的是 index.html。只要它被浏览器强缓存了,后续所有的 chunk-[hash].js 怎么变都没用,浏览器根本不会去拿新脚本。

  • JS 抛错的“断层”: 哪怕 index.html 更新了,但用户如果一直没关页面,点击跳转时去加载已经从服务器删掉的旧 hash 文件,直接就是一个 Failed to fetch,页面瞬间卡死。

结论: 靠协议(HTTP Header)只是被动防御,靠代码(JS)才是主动出击。

🌳 让页面学会自己洗澡

1. 让代码知道“我过时了”

在打包时,我通过 Vite 注入一个全局常量 __APP_VERSION__。同时,服务器会维护一个只有几百字节的 meta.json

页面每隔一分钟会悄悄去“对暗号”:

typescript
// 简单粗暴的轮询,但极为有效
const checkUpdate = async () => {
  const { version } = await fetch(`/meta.json?t=${Date.now()}`).then((r) => r.json());
  if (version !== __APP_VERSION__) {
    // 发现新版本,不再等用户刷新,我们帮他刷
    notifyAndReload();
  }
};

2. 入口文件“永不回头”

以前我为了性能给所有文件加缓存,后来在 Nginx 里,我给 index.html 判了 “死刑”:

nginx
location = /index.html {
  # 彻底禁用强缓存,确保每次都是最新的暗号
  add_header Cache-Control "no-cache, no-store, must-revalidate";
}

3. 异常捕获的“最后一道防线”

这是最能提升体验的一点。

如果用户正在操作时,旧资源 404 了,我的代码会捕捉到这个 Runtime Error

typescript
window.addEventListener(
  'error',
  (e) => {
    if (isChunkLoadFailed(e)) {
      // 发现是资源加载失败,很可能是发版了。
      // 给 URL 带个时间戳,强制拉回最新版本
      location.href = addTimestamp(location.href);
    }
  },
  true
);

🌳 避坑碎碎念

在落地这套方案时,我也踩了几个坑,记录在这里备忘:

  • 轮询成本: 刚开始担心一分钟一次轮询会压垮服务器。后来一算,meta.json 只有几百字节,CDN 成本几乎为零,反倒是省下的沟通成本(解释为什么白屏)要贵得多。
  • 切回前台: 很多移动端用户是切到后台再回来的。所以我加上了 visibilitychange 监听,用户只要一回到页面,立刻做一次版本校验,这种“即插即用”的感觉非常丝滑。
  • 灰度回滚: 现在的习惯是发版先切 5% 的流量。如果 Sentry 监控到报错率飙升,直接在 CDN 层把 index.html 回滚,用户侧因为有“自感知”逻辑,会自动退回到稳健版。

🌳 总结

“清缓存” 本质上是技术债转嫁。

作为一个前端,能用代码解决的麻烦,就别去麻烦用户。现在看着后台几乎消失的 Loading Error 监控,以及产品经理再也没问过的 “为什么没更新”,这种掌控感才是写代码最爽的地方。

正如那句话:最好的 UI 体验,是用户感觉不到 UI;最好的版本更新,是用户感觉不到更新。

最后更新于: