前端缓存策略实战指南从HTTP缓存到Service Worker的完整落地方案

Newb.潇郡 前端 阅读 840
赞 27 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

缓存这玩意儿,我干了六年前端,前三年基本靠 Ctrl+R 和清浏览器缓存硬扛。后来上线一个活动页,用户反馈“点进去还是旧版本”,运维查 CDN、我查构建产物、产品说“你是不是没发版”,折腾两小时才发现是 index.html 被强缓存了 1 小时——而我们的 JS/CSS 都带 hash,但 HTML 没变,浏览器直接从磁盘读了旧的。

前端缓存策略实战指南从HTTP缓存到Service Worker的完整落地方案

从此我给自己立了个铁律:HTML 必须协商缓存(ETag/Last-Modified),JS/CSS 必须强缓存(max-age=31536000),图片按需,API 统一不缓存。不是理论最优,是线上出过三次事故后,我亲手锤出来的底线。

重点说下最常改又最容易翻车的:静态资源缓存头。我们用 Nginx 部署,这是我现在项目里实际在跑的配置片段:

location ~* .(js|css|json|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$ {
    expires 1y;
    add_header Cache-Control "public, immutable, max-age=31536000";
}

location = /index.html {
    add_header Cache-Control "no-cache, must-revalidate, max-age=0";
    add_header ETag "";
}

注意两点:第一,immutable 这个指令我加了,虽然 Safari 早期不支持,但现在覆盖率够了。它告诉浏览器:“这个文件只要 URL 没变,内容就绝不会变”,避免在 max-age 期内还发 If-None-Match 请求。第二,/index.html 我禁用 ETag(add_header ETag ""),因为某些老旧 Nginx 版本在开启 gzip 后会生成不一致的 ETag,导致协商缓存失效,反而多一次 304 —— 我踩过两次,一次是测试环境压测时 QPS 上不去,一次是灰度发布时部分用户卡在老登录页。

构建工具侧我也做了配合。Vite 项目里,我在 vite.config.ts 加了这段:

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: assets/[name].[hash].js,
        chunkFileNames: assets/[name].[hash].js,
        assetFileNames: assets/[name].[hash].[ext]
      }
    }
  }
})

注意,这里没用 [contenthash],而是统一用 [hash]。为什么?因为 Vite 的 [contenthash] 在 CSS 中引用字体或图片时,会因路径解析顺序问题偶尔不更新——我们有个组件库用了 @font-face,升级后字体没变,但 CSS 文件 hash 变了,结果字体文件还是旧的缓存,页面字体突然回退成系统默认。换成 [hash] 后,整个 chunk 重算,问题消失。代价是略多打几个字节,但比半夜被 call 起来查缓存强。

这几种错误写法,别再踩坑了

下面这几个,都是我或同事亲手写出来、线上炸过的:

  • 给所有静态资源配 Cache-Control: public, max-age=600:美其名曰“保险”,结果每次发版 JS 更新了,用户刷新页面还是走旧缓存,要等 10 分钟才生效。更糟的是,如果用户刚打开页面,JS 已加载一半,这时候发版,新旧 JS 混合执行,报错直接白屏。
  • 在 HTML 里写 <script src="app.js?v=1.2.3"></script> 然后缓存 JS 一年:看起来能强制更新,但 v 参数是手动改的,CI 流程一旦漏掉,就永远卡死。我们曾经有次合并 PR 漏了改 v,上线后三天没人发现,直到运营说“抽奖按钮点了没反应”——其实 JS 报错了,但控制台被屏蔽了(别问为啥)。
  • API 接口加 Cache-Control: public, max-age=300:以为“缓存 5 分钟省点服务器压力”。结果用户 A 提交订单成功,B 紧接着查订单列表,返回空——因为 B 的请求命中了 A 之前的缓存响应。后来我们全量加了 Cache-Control: no-store,连代理都不许缓存。
  • 用 service worker 做全站缓存,但没处理好 updateReady 逻辑:SW 安装新版本后,直接跳转到新页面,旧页面里的 Vue 实例还在跑,新页面又起一套,内存暴涨。我们后来改成只缓存图片和字体,JS/CSS 仍走 HTTP 缓存,SW 只负责离线 fallback——简单,少坑。

实际项目中的坑

不止是代码和配置,还有些“人祸”型问题:

CDN 缓存覆盖 HTTP 头:我们用的某家 CDN,默认把所有 .js 文件缓存 30 天,不管你后端返回啥 Cache-Control。上线前我光测了 Nginx,忘了登 CDN 后台关掉自动缓存策略,结果新版 JS 发了,CDN 还在吐旧的。现在我的 checklist 第一条就是:“CDN 缓存策略是否已同步后端 header”。

开发环境本地 server 不走缓存,但测试环境走了:Webpack Dev Server 默认不设缓存头,开发者感觉“每次改完立刻生效”,一上测试环境就懵了。我现在的做法是:开发时加个 devServer.headers 强制设 Cache-Control: no-store,让本地行为跟线上一致,早发现问题。

第三方 SDK 自己乱设缓存:比如某统计 SDK 的 JS,通过 document.write 注入,它的 URL 是动态拼的,但内部资源(如上报 endpoint)却用了固定域名 + 固定路径,且服务端返回了 max-age=86400。我们没法改它,只能在 Nginx 里单独 match 它的路径,强制覆盖缓存头。这种事没法防,只能上线后盯着监控看 304 比例有没有异常飙升。

最后提一句:缓存不是越久越好。我们有个管理后台,首页 JS 打包后 1.2MB,缓存一年确实快,但用户第一次进要下 1.2MB,移动端流量党直接退出。现在我们把首页拆成按需加载,首屏 JS 控制在 200KB 内,缓存 1 年;非首屏模块用 import() + Cache-Control: public, max-age=31536000,体验和维护性都更稳。

以上是我总结的最佳实践,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,比如怎么用 Cache API 做精准资源预取、怎么结合 Webpack stats 做缓存命中率分析,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
Air-怡博
这篇文章帮我建立了正确的技术价值观,明白了什么是真正的技术实力。
点赞 4
2026-02-12 22:25