HTTP 缓存实战笔记
强缓存、协商缓存、Cache-Control 的各个指令到底怎么用,以及静态资源与接口的缓存策略该如何落地。
前端性能优化里,最「性价比」最高的一步往往不是压缩代码,而是把缓存配对。一次请求能省下的时间可能只有几百毫秒,但缓存命中时省下的是整个网络往返。本文记录我对 HTTP 缓存的实战理解,从判断流程到指令细节,再到不同资源的配置建议。
两类缓存:强缓存与协商缓存
HTTP 缓存分为两类,区别在于「要不要问服务器」:
| 类型 | 触发方式 | 是否发请求 | 生效时状态码 |
|---|---|---|---|
| 强缓存 | 命中了 Cache-Control 的有效期 |
否,直接读本地 | 200(from memory/disk cache) |
| 协商缓存 | 强缓存过期后,用条件请求询问 | 是,但体积极小 | 304(Not Modified) |
一次完整的判断流程:浏览器拿到响应后先看 Cache-Control 的 max-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-cache 和 no-store:no-cache 不是「不缓存」,而是「缓存但每次都要验证」,它和 max-age=0 效果几乎等价;no-store 才是真正的「彻底不存」。面试题常考,实战里也常有人写反。
动态接口:no-store 与短时效的取舍
对用户敏感数据(余额、购物车、登录态)直接用 no-store,防止任何一层缓存造成脏数据:
HTTP/1.1 200 OK
Cache-Control: no-store
对时效性要求没那么高、但又不希望每次请求都打到源站的接口(比如排行榜、公告),可以用短 max-age 或 no-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-Control 的 max-age(相对时间),它不依赖双方时钟一致。
小结
缓存策略的落地其实就三句话:
- HTML:
no-cache,保证页面本身总是验证。 - 带指纹的静态资源:
public, max-age=31536000, immutable。 - 动态接口:拿不准就
no-store。
把这三条配置对,页面首屏的二次访问体验会有质的提升。缓存配置是后端配合的活,但作为前端,搞清楚这些头字段的作用,才能和运维、后端同学在同一个频道上对话。