← 返回博客列表

HTTP 缓存实战笔记

强缓存、协商缓存、Cache-Control 的各个指令到底怎么用,以及静态资源与接口的缓存策略该如何落地。

前端性能优化里,最「性价比」最高的一步往往不是压缩代码,而是把缓存配对。一次请求能省下的时间可能只有几百毫秒,但缓存命中时省下的是整个网络往返。本文记录我对 HTTP 缓存的实战理解,从判断流程到指令细节,再到不同资源的配置建议。

两类缓存:强缓存与协商缓存

HTTP 缓存分为两类,区别在于「要不要问服务器」:

类型 触发方式 是否发请求 生效时状态码
强缓存 命中了 Cache-Control 的有效期 否,直接读本地 200(from memory/disk cache)
协商缓存 强缓存过期后,用条件请求询问 是,但体积极小 304(Not Modified)

一次完整的判断流程:浏览器拿到响应后先看 Cache-Controlmax-age,在有效期内直接命中强缓存;过期后再带上 If-None-Match(对应 ETag)或 If-Modified-Since(对应 Last-Modified)去问服务器,服务器返回 304 则继续用本地副本。

Cache-Control:一句话看懂各个指令

指令 含义
max-age=3600 缓存 1 小时(相对响应时间)
s-maxage=3600 共享缓存(如 CDN)的覆盖有效期
no-cache 可以缓存,但每次使用前必须向服务器验证
no-store 完全不缓存,任何环节都不得存储
immutable 有效期内的内容绝不变化,无需重新验证
private / public 仅私有缓存可存 / 任何缓存(含 CDN)可存

最容易混淆的是 no-cacheno-storeno-cache 不是「不缓存」,而是「缓存但每次都要验证」,它和 max-age=0 效果几乎等价;no-store 才是真正的「彻底不存」。面试题常考,实战里也常有人写反。

动态接口:no-store 与短时效的取舍

对用户敏感数据(余额、购物车、登录态)直接用 no-store,防止任何一层缓存造成脏数据:

HTTP/1.1 200 OK
Cache-Control: no-store

对时效性要求没那么高、但又不希望每次请求都打到源站的接口(比如排行榜、公告),可以用短 max-ageno-cache

Cache-Control: no-cache   /* 每次都验证,源站压力仍可控 */

个人经验:接口缓存宁可保守。缓存导致的「数据不一致」bug 极其隐蔽,排查成本远高于那点性能收益。拿不准时选 no-store

静态资源:指纹 + 永久缓存

静态资源(JS/CSS/图片)是缓存策略最理想的对象,前提是文件名带内容指纹——内容一变,文件名就变,URL 也就变了:

Cache-Control: public, max-age=31536000, immutable

immutable 告诉浏览器:这一年里这个 URL 的内容绝不会变,无需再验证。配合构建工具(Vite、webpack)输出的 app.8f3k2d.js 这类指纹文件名,老版本文件自然过期、新版本天然生效,从根上避免了「改代码不清缓存」的经典问题。

这也是为什么「在 HTML 上设长缓存」是个错误:HTML 没有指纹,一旦更新,用户拿到的还是旧页面。正确做法是 HTML 用 no-cache,引用的静态资源用指纹 + 永久缓存

/* HTML */
Cache-Control: no-cache

/* 带指纹的静态资源 */
Cache-Control: public, max-age=31536000, immutable

ETag 与 Last-Modified:协商缓存的细节

强缓存过期后,协商缓存兜底。Last-Modified 基于文件修改时间,粒度到秒,且某些场景下时间不准确;ETag 基于内容哈希,更精确,是更优选择。服务器返回:

HTTP/1.1 200 OK
ETag: "a1b2c3d4"
Last-Modified: Tue, 04 Aug 2026 08:00:00 GMT

浏览器下次请求自动带上:

If-None-Match: "a1b2c3d4"
If-Modified-Since: Tue, 04 Aug 2026 08:00:00 GMT

内容没变时服务器返回 304 和空的响应体,带宽成本几乎为零。ETag 的优先级高于 Last-Modified,两者都返回时以 ETag 为准。

踩坑记录

按我踩过的频率排序:

1. 开发环境缓存地狱。 改了代码刷新还是旧内容,十有八九是浏览器强缓存了 localhost 的响应。开发服务器一般已处理,但如果你自己起了静态服务,记得加上 Cache-Control: no-store,否则会被「旧页面」折磨一整天。

2. 代理层忽略 private。 Cache-Control: private 只保证「共享缓存不得存」,如果你的 Nginx / CDN 配置了强缓存指令(如 proxy_cache 且没遵守响应头),私有内容仍可能被缓存。敏感接口上 no-store 更保险。

3. 时间同步问题。 服务器与客户端时钟差异过大时,Expires(老字段)和 Last-Modified 会给出错误判断。这也是为什么现在推荐统一用 Cache-Controlmax-age(相对时间),它不依赖双方时钟一致。

小结

缓存策略的落地其实就三句话:

  • HTMLno-cache,保证页面本身总是验证。
  • 带指纹的静态资源public, max-age=31536000, immutable
  • 动态接口:拿不准就 no-store

把这三条配置对,页面首屏的二次访问体验会有质的提升。缓存配置是后端配合的活,但作为前端,搞清楚这些头字段的作用,才能和运维、后端同学在同一个频道上对话。