Your Cache Headers Could Probably be More Aggressive
您的缓存标头可能更具侵略性
https://macarthur.me/posts/more-aggressive-cache-headers
HTTP 缓存的核心并不复杂,本质上解决两个问题:
- 缓存还能不能直接使用?
- 不能直接使用时,资源有没有发生变化?
第一次请求资源后,浏览器可能保存完整的 HTTP Response(Headers + Body)。
之后再次请求时,先判断缓存的新鲜度。新鲜则直接使用;过期则向服务器验证。
资源未变化服务器返回 304,否则返回 200 + body。
flowchart TB
s([准备发起请求]) --> hasCache{有缓存?}
hasCache -- 否 --> request[发起请求] --> e([结束])
hasCache -- 是 --> isCacheExpired{缓存过期?}
isCacheExpired -- 否 --> readCache[读取缓存] --> e
isCacheExpired -- 是 --> hasEtag{有 ETag?}
hasEtag -- 是 --> addHeaderIfNoneMatch[添加 If-None-Match 头] --> request
hasEtag -- 否 --> hasLastModified{有 Last-Modified?}
hasLastModified -- 是 --> addHeaderIfModifiedSince[添加 If-Modified-Since 头] --> request
hasLastModified -- 否 --> request
request --> response{响应?}
response -- 是 --> e
response -- 否 --> readCache
一、HTTP 缓存相关 Header
现代 HTTP 缓存主要围绕以下 Header 工作:
| Header | 作用 |
|---|---|
Cache-Control |
定义缓存策略和新鲜度 |
ETag |
标识资源版本 |
Last-Modified |
标识资源最后修改时间 |
If-None-Match |
根据 ETag 进行条件请求 |
If-Modified-Since |
根据修改时间进行条件请求 |
Expires |
HTTP/1.0 时代的过期时间 |
Pragma |
历史兼容机制 |
其中最核心的是:
Cache-Control
ETag / Last-Modified
If-None-Match / If-Modified-Since
Expires 和 Pragma 主要用于历史兼容,现代应用优先使用 Cache-Control。
二、HTTP Header 常见问题
1. no-cache 是不是不缓存?
不是。
表示允许缓存,但使用前必须重新验证。
no-store 才表示禁止存储响应。
| 指令 | 允许存储 | 使用前验证 |
|---|---|---|
no-cache |
是 | 是 |
no-store |
否 | 不适用 |
2. max-age 控制浏览器还是 CDN?
都可能。
Cache-Control: public, max-age=60, s-maxage=3600
通常表示普通缓存的新鲜时间为 60 秒,而共享缓存(如 CDN)可以使用 s-maxage 的 3600 秒。
3. ETag 是不是文件 MD5?
不是。
ETag 只是服务器定义的实体标签,可以由 Hash、版本号等方式生成。
4. 304 是不是重新返回资源?
不是。
200 → 返回资源
304 → 资源没变化,继续使用已有缓存
304 不重新传输原来的 Response Body。
5. 浏览器刷新为什么还可能出现 304?
刷新不等于清空缓存。浏览器可能要求重新验证缓存,于是发送:
If-None-Match: "abc123"
服务器确认资源没有变化后返回 304 Not Modified。
具体行为还取决于浏览器、刷新方式以及开发者工具设置。
三、强缓存
“强缓存”不是 HTTP 标准术语,而是开发领域对缓存新鲜时直接使用这一行为的称呼。
例如:
Cache-Control: max-age=3600
第一次请求:
Browser → Server → 200 + Resource
↓
Browser Cache
缓存仍然新鲜时:
Browser → Browser Cache
无需向服务器发送请求。
因此:强缓存解决的是“要不要发网络请求”。
需要注意,缓存过期并不意味着缓存立即被删除,而是通常不能再无条件使用,需要进入重新验证流程。
四、协商缓存与 304
缓存过期后,客户端可以利用 ETag 或 Last-Modified 发起条件请求。
- ETag 可以理解成文件版本:
例如第一次请求服务器返回 ETag: "abc123"
后续的请求就可以带上 If-None-Match: "abc123"
服务器比对本地资源 ETag 就可以根据资源变化情况返回 304 或 200 + 新 ETag。
Last-Modified是变更时间:
# 对应 ETag
Last-Modified: Tue, 08 Sep 2026 10:00:00 GMT
# 对应 If-None-Match
If-Modified-Since: Tue, 08 Sep 2026 10:00:00 GMT
总之,服务器端和浏览器根据这些头来协商缓存,确认资源有没有变化,以及是否需要重新传输 Body。
五、浏览器与 CDN 的多级缓存
生产环境通常不是:
Browser → Origin
而是:
Browser → CDN → Origin
甚至:
Browser → Proxy → CDN → Origin
浏览器缓存命中时,请求根本不会到达 CDN。
浏览器缓存过期后,请求到 CDN;如果 CDN 仍然有新鲜缓存,也无需回源。
因此每一层都可能独立判断:是否存在缓存?缓存是否 Fresh?是否需要重新验证?是否需要继续向下一层请求?
这也是理解 CDN 缓存的基础。
六、生产环境缓存策略
不同资源应该采用不同策略。
- HTML:让浏览器保存,但使用前重新验证,及时获取最新入口:
Cache-Control: no-cache -
JS / CSS
如果文件名带内容 Hash,比如
app.8f3a2c.js,可以长期缓存:Cache-Control: public, max-age=31536000, immutable内容变化后生成新的 URL,从根本上避免旧版本缓存问题。
-
公共 API:根据实时性设置:
Cache-Control: public, max-age=60 - 用户私有数据:
Cache-Control: private
敏感数据则考虑:Cache-Control: no-store