很多人遇到过同一个现象:前端明明改完了,图片也换了,自己在浏览器里清一下缓存就一切正常,可客服还是会隔三差五收到「样式错乱」「图片还是旧的」这类反馈。多数时候并不是文件没发上去,而是浏览器或 CDN 还在用本地缓存里的旧副本。缓存本身是好事,它省带宽、也让二次访问更快;麻烦在于文件更新了,缓存并不知道。
先分清缓存发生在哪一层
从访客到源站,中间可能叠了好几层:浏览器缓存、CDN 边缘节点、反向代理(Nginx、Varnish 之类),最后才轮到应用本身。排查时按「由近到远」的顺序看,比一上来就重启服务器有效得多。
- 浏览器:开发者工具的网络面板看 Size 一列,出现 memory cache 或 disk cache 就说明命中了本地;
- CDN:看响应头里的 X-Cache、Age、CF-Cache-Status 之类的字段;
- 反向代理:看代理层的缓存目录与访问日志;
- 源站:以上都没命中,请求才会真正打到后端。
把「这次请求停在哪一层」先确定下来,后面的动作才有针对性。
缓存头里真正需要关心的字段
Cache-Control
优先级最高的控制项。max-age 决定文件在缓存里能活多久;no-cache 并不是不缓存,而是每次使用前都要回源校验;no-store 才是彻底不落盘。对已经带版本号或指纹的静态资源,可以放心设长一些;对 HTML 入口文件,通常设短时间或每次校验,免得入口被卡在旧版本上。
ETag 与 Last-Modified
这两个是给回源校验用的。文件没变就返回 304,省下传输体积。需要留意一种情况:如果同一文件在多台机器上算出的 ETag 不一致(比如集群里拿 inode 当依据),校验会反复失败,反而拖慢。遇到这种表现,可以统一 ETag 的生成规则,或者干脆只依赖 Last-Modified。
Expires
属于较早的写法。它和 Cache-Control 同时存在时容易互相打架,建议只保留一处,并且表意一致,别一边写缓存一年、一边写不缓存。
版本号加在哪里才算数
常见做法是给文件名或查询串加指纹,例如 app.3f9a2c.js 或 app.js?v=3f9a2c。几个要点值得记一下:
- 文件内容没变就不要动版本号,否则每次发布都让全部访客重新下载一遍;
- 入口 HTML 里引用的资源路径要一起改,别只换了资源却漏了引用;
- 尽量别拿发布日期或时间戳当版本号,除非你确实每次都有真实变更;
- CDN 或对象存储上的旧文件先别急着删,留一段时间给还没刷新的访客。
一份可以直接照做的自查清单
- 抽查首页、栏目页、详情页的 HTML 响应头,确认缓存时间符合预期;
- 抽查 JS、CSS、图片的响应头,确认是否带长缓存和指纹;
- 检查有没有同名文件被覆盖、版本号却没变的情况;
- 确认 CDN 刷新策略:是全量刷新还是按目录刷新,谁有操作权限;
- 确认回源时缓存头是否被正确透传,有的代理会顺手改写;
- 完整记录一次「改文件—发布—验证」,看看各层分别花了多久才更新。
发布节奏上的两个小习惯
一是把 HTML 和静态资源分开对待:HTML 走短缓存,资源走长缓存,这样内容更新能马上生效,资源又不至于反复下载。二是动手前先问一句「这次改的是内容还是文件本身」,如果只是文案微调,通常不需要动版本号,改了反而增加无谓的流量。
缓存问题的排查顺序永远是:先弄清请求命中了哪一层,再决定清哪一层。盲目清缓存只能解决一时,下次还会遇到。
最后提醒一句,缓存策略不是设一次就完事。换 CDN、调服务器、换构建工具,都可能悄悄改变响应头。把它固定成上线检查里的一个环节,以后能省下不少解释成本。