基于 RFC 9111 (2022) 的 HTTP 缓存标准解读


1. 概述

HTTP 缓存是 Web 性能优化的基石,通过复用已获取的响应来减少网络请求、降低延迟、节省带宽。

1.1 缓存链架构

┌─────────┐      ┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│ Browser │ ←→  │ Shared Cache│ ←→  │ Shared Cache│ ←→  │ Origin      │
│ Private │      │ (CDN/Proxy) │      │ (CDN/Proxy) │      │ Server      │
│  Cache  │      │             │      │             │      │             │
└─────────┘      └─────────────┘      └─────────────┘      └─────────────┘
     ↑                                                     
     │ Age header from shared caches                       
     ↓                                                     
Browser 计算: age = time_in_browser + Age:xxx header value

关键概念

  • 私有缓存(Private Cache):浏览器本地缓存,仅服务于单个用户
  • 共享缓存(Shared Cache):CDN、代理服务器等,可服务多个用户
  • Age 头:中间缓存告知客户端响应已在缓存中存放的秒数

1.2 Cache-Control 机制

Cache-Control 是控制缓存行为的主要 HTTP 头,支持多种指令:

HTTP/2 200
Cache-Control: max-age=3600, must-revalidate, public

指令分类

目标指令类型
私有缓存privateno-storeimmutable
共享缓存s-maxageproxy-revalidatepublic
两者皆可max-ageno-cachemust-revalidatestale-while-revalidate

2. 新鲜度判定:缓存何时可用?

当缓存收到请求时,需要判断缓存响应是新鲜(fresh)还是过期(stale)

2.1 缓存决策流程

                    ┌─────────────────┐
                    │ 收到HTTP请求     │
                    └────────┬────────┘
                             ↓
                    ┌─────────────────┐
                    │ 查找缓存响应     │
                    └────────┬────────┘
                             ↓
              ┌──────────────┴──────────────┐
              ↓              ↓               ↓
         ┌─────────┐   ┌──────────┐   ┌──────────┐
         │ 找到    │   │ 未找到   │   │ 找到但   │
         │ 匹配响应│   │          │   │ 不匹配   │
         └────┬────┘   └────┬─────┘   └────┬─────┘
              ↓              ↓               ↓
    ┌─────────┴─────────┐    ↓    ┌────────┴────────┐
    ↓                   ↓         ↓                 ↓
┌────────────┐    ┌──────────┐ ┌────────────┐  ┌──────────┐
│ 计算age    │    │ 向服务器 │ │ 重新查找   │  │ 转发请求 │
│ vs freshness│   │ 发起请求 │ │ (Vary等)   │  │ 到服务器 │
└─────┬──────┘    └──────────┘ └─────┬─────┘  └──────────┘
      ↓                              │
      ↓  ┌───────────────────────────┘
      ↓  ↓
┌─────────────┐
│ age <=      │←── max-age / s-maxage / Expires
│ freshness?  │
└──────┬──────┘
       ↓ NO
┌─────────────┐
│ 检查是否可验证│
│ (must-rev.) │
└──────┬──────┘
       ↓
┌─────────────┐    ┌──────────────────┐
│ 发起条件请求 │───→│ If-None-Match    │
│ (验证)      │    │ If-Modified-Since│
└──────┬──────┘    └──────────────────┘
       ↓
┌──────────────┐
│ 304?         │←── ETag / Last-Modified 匹配
└──────┬───────┘
   NO  ↓        YES ↓
┌────────────┐  ┌─────────────┐
│ 200 + Body │  │ 304 无 Body │
└────────────┘  │ 复用缓存     │
                └─────────────┘

2.2 新鲜度时间线判定顺序

┌─────────────────────────────────────────────────────────────────┐
│                    新鲜度时间线判定顺序                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. Cache-Control: max-age=N                                   │
│     └─→ 直接使用 max-age 作为新鲜度秒数                          │
│                                                                 │
│     ↓ 不存在                                                    │
│                                                                 │
│  2. Expires: <date> 与 Date: <date> 的差值                      │
│     └─→ 新鲜度 = Expires - Date                                 │
│                                                                 │
│     ↓ 不存在                                                    │
│                                                                 │
│  3. Last-Modified 启发式估算                                    │
│     └─→ 新鲜度 ≈ (Date - Last-Modified) / 10                    │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│  共享缓存优先规则: s-maxage 优先于所有其他指令                    │
└─────────────────────────────────────────────────────────────────┘

2.3 新鲜度 vs 过期

状态条件行为
Freshage <= freshness直接使用缓存,不发起请求
Staleage > freshness必须验证后才能使用

3. 过期后处理:条件请求验证

即使响应过期,也不应直接丢弃,而应通过条件请求验证是否仍可使用。

3.1 条件请求验证流程

┌─────────────────────┐      ┌─────────────────────┐
│  客户端缓存的响应     │      │      源服务器        │
├─────────────────────┤      ├─────────────────────┤
│ HTTP/1.1 200 OK     │      │                     │
│ ETag: "abc123"      │      │                     │
│ Last-Modified: ...  │      │                     │
│ Content: {...}      │      │                     │
└─────────┬───────────┘      └──────────┬──────────┘
          │                            │
          │  缓存过期,发送条件请求      │
          ├───────────────────────────→│
          │ GET /api/data              │
          │ If-None-Match: "abc123"    │
          │ If-Modified-Since: ...     │
          │                            │
          │         ┌──────────────────┴───┐
          │         ↓                      ↓
          │    ┌──────────┐           ┌──────────┐
          │    │ 内容未变  │           │ 内容已变  │
          │    │ ETag匹配  │           │           │
          │    └────┬─────┘           └────┬─────┘
          │         ↓                      ↓
          │    ┌──────────┐           ┌──────────┐
          │    │ 304 Not  │           │ 200 OK   │
          │    │ Modified │           │ 新内容    │
          │    └────┬─────┘           └────┬─────┘
          │         │                      │
          │         │  复用原缓存           │  使用新内容
          │         │  更新headers         │
          │         ↓                      │
          │    ┌──────────┐                │
          │    │ 缓存有效  │                │
          │    │ 无需下载  │                │
          │    │ 节省带宽  │                │
          │    └──────────┘                │
          │                                 │
          └─────────────────────────────────┘

3.2 验证标签与条件头对应关系

┌────────────────────┬─────────────────────────────┐
│  响应头 (服务器)     │  条件请求头 (客户端)         │
├────────────────────┼─────────────────────────────┤
│ ETag: "abc123"     │ If-None-Match: "abc123"     │
│ Last-Modified: ... │ If-Modified-Since: ...      │
└────────────────────┴─────────────────────────────┘
              ↑
              │ 优先级规则
              ↓
   If-None-Match 优先于 If-Modified-Since
   (两者都存在时,只评估 If-None-Match)

验证结果

服务器响应含义处理
304 Not Modified内容未变复用缓存,更新 headers
200 OK内容已更新使用新响应

4. 缓存存储机制

4.1 缓存键(Cache Key)

HTTP 缓存使用缓存键来存储和检索响应:

┌─────────────────────────────────────────────────────────┐
│                   缓存键组成                             │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  基本缓存键 = URL + HTTP Method                          │
│                                                         │
│  示例:                                                  │
│  ├── GET https://api.example.com/data → cache key 1     │
│  ├── POST https://api.example.com/data → cache key 2    │
│  └── GET https://api.example.com/data → cache key 1     │
│                                                         │
├─────────────────────────────────────────────────────────┤
│  浏览器额外分区 (隐私保护,双键机制):                      │
│                                                         │
│  缓存键 += top-level-site + frame-site + flags          │
│                                                         │
│  目的: 防止 iframe 跨站追踪用户                           │
└─────────────────────────────────────────────────────────┘

4.2 Vary 响应头:处理可变响应

同一 URL 可能根据请求头返回不同内容,Vary 头指示哪些请求头影响响应。

请求: GET /api/resource
     Accept-Language: zh-CN

┌─────────────────────────────────────────────────────┐
│                   共享缓存                           │
│  ┌─────────────────────────────────────────────────┐│
│  │ Cache Key = "GET" + "/api/resource" + "zh-CN"  ││
│  │                                                 ││
│  │ ┌─────────────────────────────────────────────┐ ││
│  │ │ 响应1: zh-CN → {"greeting": "你好"}         │ ││
│  │ │ Vary: Accept-Language                        │ ││
│  │ └─────────────────────────────────────────────┘ ││
│  │ ┌─────────────────────────────────────────────┐ ││
│  │ │ 响应2: en-US → {"greeting": "Hello"}        │ ││
│  │ │ Vary: Accept-Language                        │ ││
│  │ └─────────────────────────────────────────────┘ ││
│  │ ┌─────────────────────────────────────────────┐ ││
│  │ │ 响应3: fr-FR → {"greeting": "Bonjour"}      │ ││
│  │ │ Vary: Accept-Language                        │ ││
│  │ └─────────────────────────────────────────────┘ ││
│  └─────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────┘

请求: GET /api/resource
     Accept-Language: en-US  ← 不同的Vary值 → 命中响应2

Vary 头的问题

问题影响
高变异性大量不可复用的缓存变体
CDN 行为差异部分 CDN 忽略或不正确处理 Vary

5. Cache-Control 响应指令详解

┌──────────────────────────────────────────────────────────────┐
│                    响应指令速查                               │
├──────────────────────────────────────────────────────────────┤
│  新鲜度控制                                                    │
├──────────────────────────────────────────────────────────────┤
│  max-age=<seconds>     → 定义新鲜度持续时间                    │
│  s-maxage=<seconds>    → 仅共享缓存使用,优先级最高            │
│  immutable             → 期内不会更新,跳过软刷新验证          │
├──────────────────────────────────────────────────────────────┤
│  验证控制                                                      │
├──────────────────────────────────────────────────────────────┤
│  no-cache              → 每次使用前必须验证                    │
│  must-revalidate       → 过期后必须验证,不能使用过期缓存      │
│  proxy-revalidate      → 仅共享缓存需验证                      │
├──────────────────────────────────────────────────────────────┤
│  存储控制                                                      │
├──────────────────────────────────────────────────────────────┤
│  no-store              → 禁止任何缓存存储                      │
│  private               → 仅浏览器私有缓存可用                  │
│  public                → 允许共享缓存存储认证请求的响应         │
├──────────────────────────────────────────────────────────────┤
│  其他控制                                                      │
├──────────────────────────────────────────────────────────────┤
│  stale-while-revalidate=<N> → 后台验证期间可返回过期内容N秒   │
│  stale-if-error=<N>           → 验证失败时可返回过期内容N秒   │
│  no-transform              → 禁止中间件转换内容               │
│  must-understand           → 只缓存已知状态码                 │
└──────────────────────────────────────────────────────────────┘

5.1 新鲜度指令

max-age=<number>

┌─────────────────────────────────────────┐
│  max-age=3600                           │
│  ↓                                      │
│  ┌─────────────────────────────────┐   │
│  │ 0s    1800s    3600s    5400s   │   │
│  │  │      │         │        │    │   │
│  │ Fresh←─┼─────────→Stale←───┼──→ │   │
│  │        │                  │    │   │
│  └────────┴──────────────────┴────┘   │
│         ↑                             │
│         响应在3600秒内为Fresh          │
└─────────────────────────────────────────┘

immutable

┌─────────────────────────────────────────────┐
│  Cache-Control: max-age=31536000, immutable │
│                                             │
│  用户操作: 软刷新 (Ctrl+R)                   │
│  ┌───────────────────────────────────────┐ │
│  │ 直接使用缓存,不发起验证请求            │ │
│  │ (浏览器知道内容期内不会更新)            │ │
│  └───────────────────────────────────────┘ │
│                                             │
│  用户操作: 硬刷新 (Ctrl+Shift+R)            │
│  ┌───────────────────────────────────────┐ │
│  │ 强制发起请求,忽略缓存                  │ │
│  └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

5.2 验证指令

must-revalidate vs no-cache

┌────────────────┬─────────────────────────────┐
│  指令          │  行为                        │
├────────────────┼─────────────────────────────┤
│  no-cache      │ 始终验证后才能使用           │
│                │ (相当于 max-age=0,          │
│                │  must-revalidate)           │
├────────────────┼─────────────────────────────┤
│  must-revalidate│ 仅过期后必须验证             │
│                │ 未过期时可直接使用           │
└────────────────┴─────────────────────────────┘

must-revalidate 边界行为

过期缓存 + must-revalidate:

  场景1: 验证成功 (304)  →  复用缓存
  场景2: 验证成功 (200)  →  使用新内容
  场景3: 验证失败 (5xx)  →  返回错误,不能使用过期缓存
  场景4: 离线            →  返回 504 Gateway Timeout

5.3 存储指令

no-store

┌─────────────────────────────────────────────────────────┐
│  Cache-Control: no-store                               │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │ 缓存处理流程                                     │   │
│  │                                                 │   │
│  │  收到响应 ──→ 不存储 ──→ 转发给客户端            │   │
│  │                         ↓                       │   │
│  │                   立即从内存清除                  │   │
│  │                                                 │   │
│  │  ⚠️ 隐私保护不依赖于 no-store                    │   │
│  │  ⚠️ 恶意缓存可能不遵守此指令                      │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

private vs public

┌────────────────┬─────────────────────────────┐
│  指令          │  缓存允许                    │
├────────────────┼─────────────────────────────┤
│  private       │  ✓ 浏览器私有缓存            │
│                │  ✗ 共享缓存 (CDN/Proxy)      │
├────────────────┼─────────────────────────────┤
│  public        │  ✓ 浏览器私有缓存            │
│                │  ✓ 共享缓存                  │
│                │  ✓ 认证请求的响应            │
└────────────────┴─────────────────────────────┘

5.4 过期内容复用指令

stale-while-revalidate

┌─────────────────────────────────────────────────────────┐
│  Cache-Control: max-age=60, stale-while-revalidate=120 │
│                                                         │
│  时间线:                                                 │
│                                                         │
│  0s    60s    180s    240s    300s                     │
│   │      │      │       │       │                      │
│   │  Fresh     Stale   │       │                      │
│   │      │      │       │       │                      │
│   │      │      └───────┴───────┘                      │
│   │      │           后台验证中                        │
│   │      │           返回过期内容                       │
│   │      │                                         │
│   └──────┴─────────────────────────────────→          │
│      立即返回                                         │
│      过期内容                                         │
│      触发后台验证                                      │
└─────────────────────────────────────────────────────────┘

6. Cache-Control 请求指令

浏览器在刷新时使用请求指令表达缓存偏好:

┌──────────────────────────────────────────────────────────────┐
│                    请求指令速查                               │
├──────────────────────────────────────────────────────────────┤
│  max-age=<seconds>   → 期望响应age <= 指定秒数                │
│  max-stale=<seconds> → 接受过期时间在此范围内的响应           │
│  min-fresh=<seconds> → 期望响应至少有指定秒数的新鲜度         │
│  no-cache            → 必须验证后才能使用缓存                 │
│  no-store            → 禁止缓存存储此请求和响应               │
│  only-if-cached      → 只接受缓存,不向服务器请求             │
│  no-transform        → 禁止转换响应内容                       │
└──────────────────────────────────────────────────────────────┘

7. 浏览器刷新机制

7.1 软刷新 vs 硬刷新行为对比

┌────────────────────────────────────────────────────────────────┐
│                        软刷新 (Ctrl+R / Cmd+R)                  │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  Firefox/Chrome:                                              │
│     ├─────────────────────────────────────────────────────→   │
│     │  主资源: 带 If-None-Match/If-Modified-Since 验证       │
│     │  子资源: 按各自 Cache-Control 策略处理                  │
│     │  (带 immutable 的资源不验证)                            │
│                                                                │
│  Safari:                                                      │
│     ├─────────────────────────────────────────────────────→   │
│     │  主资源: 非条件请求 (不带验证头)                         │
│     │  子资源: 按策略处理                                     │
│                                                                │
├────────────────────────────────────────────────────────────────┤
│                       硬刷新 (Ctrl+Shift+R / Cmd+Shift+R)      │
├────────────────────────────────────────────────────────────────┤
│                                                                │
│  所有浏览器 (除 Safari 快捷键不同):                            │
│     ├─────────────────────────────────────────────────────→   │
│     │  所有资源: Cache-Control: no-cache                      │
│     │  完全跳过缓存,强制从服务器获取                          │
│     │                                                         │
│  快捷键差异:                                                   │
│     ├ Windows/Linux: Ctrl+Shift+R                             │
│     ├ macOS Chrome/Firefox: Cmd+Shift+R                       │
│     └ macOS Safari: Cmd+Option+R (Reader Mode冲突)            │
│                                                                │
└────────────────────────────────────────────────────────────────┘

7.2 immutable 指令的诞生背景

问题 (2015年):

  用户刷新页面 → 所有子资源都被重新验证 → 大量 304 响应
  ┌─────────────────────────────────────────────────────────┐
  │  场景: Facebook 用户刷新 Feed 页面                        │
  │                                                        │
  │  1 次页面请求                                            │
  │  ├─ 10+ 次脚本请求 (长期缓存的 JS)                        │
  │  ├─ 5+ 次样式请求 (长期缓存的 CSS)                        │
  │  └─ 20+ 次图片请求                                       │
  │                                                        │
  │  结果: 大量不必要的 304 响应,浪费带宽和服务器资源         │
  └─────────────────────────────────────────────────────────┘

解决方案:

  1. immutable 指令 (RFC 8246)
     └─→ 告诉浏览器: 期内不会更新,无需验证

  2. 改进的软刷新策略 (Chrome 2017, Safari, Firefox 100+)
     └─→ 仅验证主资源,子资源按策略处理

  现在: 两者并存,但改进的刷新策略已解决大部分问题

8. 认证请求响应缓存

8.1 基本规则

┌──────────────────────────────────────────────────────────────┐
│  默认规则                                                     │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  请求带 Authorization header?                                │
│         ↓ YES                                                │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ 共享缓存默认禁止存储此响应                            │    │
│  │ (因为响应是用户特定的)                                │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

8.2 允许共享缓存存储的指令

┌──────────────────────────────────────────────────────────────┐
│  以下任一指令可允许共享缓存存储认证请求的响应:                 │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  ✓ public                                                   │
│    └─→ 显式允许共享缓存存储                                  │
│                                                              │
│  ✓ s-maxage=<seconds>                                       │
│    └─→ 共享缓存专用,同时允许存储认证响应                     │
│                                                              │
│  ✓ must-revalidate                                          │
│    └─↓ 包含 s-maxage 语义                                   │
│                                                              │
│  ✗ private                                                  │
│    └─→ 阻止任何共享缓存存储                                  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

⚠️  部署 public/s-maxage/must-revalidate 前必须评估安全性!

9. 总结

9.1 缓存策略选择决策树

┌──────────────────────────────────────────────────────────────┐
│                    缓存策略选择                               │
├──────────────────────────────────────────────────────────────┤
│                                                              │
│  是否需要缓存?                                                │
│      │                                                       │
│      ├─ NO ──→ Cache-Control: no-store                       │
│      │                                                       │
│      └─ YES ──→ 是否包含敏感信息?                            │
│                │                                             │
│            YES ──→ Cache-Control: private, no-store          │
│                │                                             │
│            NO ──→ 是否需要共享缓存 (CDN)?                     │
│                │                                             │
│            YES ──→ 是否频繁更新?                             │
│                │                                             │
│            YES ──→ s-maxage=0, must-revalidate               │
│            NO  ──→ s-maxage=<seconds>, immutable?            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

9.2 推荐缓存策略

资源类型缓存策略说明
HTMLno-cache, must-revalidate每次验证,确保内容最新
静态资源 (JS/CSS)max-age=31536000, immutable长期缓存,文件名含 hash
API 响应no-caches-maxage根据更新频率选择
用户特定数据private, no-store不缓存或仅私有缓存
图片/字体max-age=<long>, immutable长期缓存

参考资料