HTTP 缓存总结

Your Cache Headers Could Probably be More Aggressive
您的缓存标头可能更具侵略性
https://macarthur.me/posts/more-aggressive-cache-headers

HTTP 缓存的核心并不复杂,本质上解决两个问题:

  1. 缓存还能不能直接使用?
  2. 不能直接使用时,资源有没有发生变化?

第一次请求资源后,浏览器可能保存完整的 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

ExpiresPragma 主要用于历史兼容,现代应用优先使用 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

缓存过期后,客户端可以利用 ETagLast-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
如果你有魔法,你可以看到一个评论框~