缓存相关
缓存控制
请求头
- Cache-Control:
- no-cache:强制向服务器验证缓存有效期(协商缓存验证)
- o-store:禁止任何缓存
- max-age=0:缓存已经过期需重新验证
- max-stale=300:允许使用过期资源但不能超过300秒的缓存
- Pragma:HTTP/1.0 遗留头(兼容用)
Pragma: no-cache 等效于 Cache-Control: no-cache
响应头
- Cache-Control
- no-store:禁止任何缓存
- no-cache:协商缓存
- private:仅允许浏览器缓存
- public:允许所有缓存(浏览器/CDN)
- max-age=3600:资源有效期3600秒
- immutable:资源永远不会过期
- must-revalidate:一旦资源过期(比如已经超过max-age),在成功向原始服务器验证之前,缓存不能用该资源响应后续请求。
- Expires:资源的绝对过期时间(HTTP/1.0 遗留)
Expires: Wed, 21 Oct 2026 07:28:00 GMT
- Age:资源在缓存中已存储的时间(秒),在代理缓存中停留的时间,以秒为单位;通常接近于 0。如果显示为 Age: 0,则表示该内容可能是从源服务器上获取的;否则,它通常是通过代理服务器当前日期与 HTTP 响应中包含的 Date 通用标头之间的差值来计算得出的。
Age: 120(由代理服务器添加)
缓存验证
请求头
- If-None-Match
- 携带缓存的 ETag 值
- 匹配时服务器返回 304 Not Modified
- 示例:
If-None-Match: "33a64df551425fcc55e4d42a148795d9"
- If-Modified-Since
- 携带缓存的 Last-Modified 时间
- 资源未修改时返回 304
- 示例:
If-Modified-Since: Mon, 08 Jul 2025 14:30:00 GMT
响应头
- Etag 资源版本标识符(如哈希值)
ETag: "33a64df551425fcc55e4d42a148795d9" - Last-Modified 资源最后修改时间
Last-Modified: Mon, 08 Jul 2025 14:30:00 GMT
辅助缓存类头部
影响缓存键和存储策略
- Vary:响应头 指定区分缓存版本的请求头
示例:
Vary: User-Agent:为不同浏览器存储独立缓存Vary: Accept-Encoding:区分压缩/未压缩版本
- Pragma 请求头 旧版等效于 Cache-Control: no-cache(现代浏览器已弃用)
缓存 HTTP 头部交互
前端缓存的核心是请求头和响应头的协同工作,下面我将通过流程和交互示例说明这些头部如何协同实现缓存控制:
缓存控制流程交互
sequenceDiagram
participant Client as 浏览器
participant Cache as 浏览器缓存
participant Server as 服务器
Note over Client: 首次请求资源
Client->>Server: GET /app.js<br>无缓存相关头部
Server-->>Client: HTTP 200 OK<br>Cache-Control: public, max-age=3600<br>ETag: "abc123"<br>Last-Modified: Wed, 21 Oct 2026 07:28:00 GMT<br>Content-Type: application/javascript
Client->>Cache: 存储响应和缓存头部
Note over Client: 60分钟后再次请求
Client->>Cache: 检查缓存
Cache-->>Client: 缓存已过期(max-age=3600)
Client->>Server: GET /app.js<br>If-None-Match: "abc123"<br>If-Modified-Since: Wed, 21 Oct 2026 07:28:00 GMT
alt 资源未修改
Server-->>Client: HTTP 304 Not Modified<br>Cache-Control: public, max-age=3600<br>(空响应体)
Client->>Cache: 更新缓存过期时间
Cache-->>Client: 返回缓存的资源
else 资源已修改
Server-->>Client: HTTP 200 OK<br>Cache-Control: public, max-age=3600<br>ETag: "def456"<br>新资源内容
Client->>Cache: 更新缓存
end
关键头部交互关系
1. 缓存存储阶段(首次请求)
请求头:无特殊缓存头
响应头:
Cache-Control: public, max-age=3600→ 指示缓存存储策略ETag: "abc123"→ 资源版本标识符Last-Modified: [date]→ 资源最后修改时间
浏览器接收到这些头部后,会将资源存入缓存并记录这些元数据。
2. 缓存验证阶段(后续请求)
当缓存过期或需要验证时:
请求头:
If-None-Match: "abc123"→ 携带上次响应的 ETag 值If-Modified-Since: [date]→ 携带上次响应的 Last-Modified 值
服务器处理:
- 比较当前资源的 ETag 与请求中的 If-None-Match
- 比较当前修改时间与 If-Modified-Since
- 如果未修改 → 返回 304 Not Modified
- 如果已修改 → 返回 200 和新资源
3. 缓存更新阶段
304响应时:
- 浏览器使用缓存资源
- 根据响应中的
Cache-Control更新缓存过期时间
200响应时:
- 浏览器使用新资源
- 根据响应头部更新缓存内容和元数据
常见缓存控制场景
场景1:强制缓存验证
# 请求头
Cache-Control: no-cache
# 响应头(无论资源是否修改)
Cache-Control: max-age=600
ETag: "xyz789"
交互:no-cache 强制跳过缓存直接向服务器验证,但服务器仍可返回缓存指令
场景2:版本化资源永久缓存
# 响应头
Cache-Control: public, max-age=31536000, immutable
ETag: "v1.0-sha256"
交互:immutable 告诉浏览器在 max-age 期间不会修改,无需验证
场景3:Vary头部控制缓存版本
# 响应头
Vary: Accept-Encoding, User-Agent
# 后续请求头
Accept-Encoding: gzip
User-Agent: Mobile
交互:浏览器会根据 Accept-Encoding 和 User-Agent 的不同值存储多个缓存版本
常见组合
- 强缓存+协商缓存组合:
Cache-Control: public, max-age=604800
ETag: "sha256-abc123"
- 静态资源永久缓存:
Cache-Control: public, max-age=31536000, immutable
- 动态内容不缓存:
Cache-Control: no-store
- 用户相关内容:
Cache-Control: private, max-age=600
Vary: Cookie
Expires 与 max-age 的区别与优先级
核心区别
| 特性 | Expires | Cache-Control: max-age |
|---|---|---|
| 标准版本 | HTTP/1.0 | HTTP/1.1 |
| 时间类型 | 绝对时间 (GMT 格式) | 相对时间 (秒数) |
| 示例 | Expires: Wed, 21 Oct 2026 07:28:00 GMT | Cache-Control: max-age=3600 |
| 时钟依赖 | 依赖客户端/服务器时间同步 | 不依赖时钟,从响应接收时刻开始计算 |
| 优先级 | 低 | 高 |
| 精度问题 | 时区转换可能导致误差 | 精确到秒 |
同时设置时的处理规则
当响应头中同时包含 Expires 和 Cache-Control: max-age 时:
max-age优先级更高:
- 浏览器会完全忽略
Expires头 - 缓存时间以
max-age值为准
- 计算方式差异:
// max-age 计算方式 (推荐)
const expirationTime = Date.now() + (maxAgeValue * 1000);
// Expires 计算方式 (不推荐)
const expirationTime = new Date(expiresValue).getTime();
- 现代浏览器行为:
- 所有主流浏览器(Chrome/Firefox/Safari/Edge)都优先采用
max-age - 仅当缺少
Cache-Control时才会回退到Expires
实际场景分析
场景1:两者设置一致
Expires: Wed, 10 Jul 2025 08:00:00 GMT
Cache-Control: max-age=86400 // 24小时
- 结果:浏览器使用
max-age=86400(忽略Expires) - 缓存有效期 = 响应接收时间 + 86400秒
场景2:两者冲突
Expires: Wed, 10 Jul 2025 08:00:00 GMT // 未来时间
Cache-Control: max-age=0 // 立即过期
- 结果:资源被视为立即过期,下次请求必须验证
- 浏览器完全忽略
Expires的未来值
场景3:仅设置 Expires
Expires: Wed, 10 Jul 2025 08:00:00 GMT
- 结果:浏览器使用
Expires时间 - 风险:如果客户端时钟错误,缓存可能提前失效或过久保留
为什么 max-age 更优秀?
- 时钟无关性:
- 不依赖客户端/服务器时间同步
- 避免时区转换错误
- 精确控制:
// 精确控制到秒级
Cache-Control: max-age=31536000 // 精确1年
// 对比 Expires 需要计算日期
Expires: Wed, 10 Jul 2026 08:15:22 GMT
- 组合指令:
// 可与其他指令组合
Cache-Control: max-age=604800, must-revalidate, public
- 解决「时间陷阱」:
- 当
Expires设置为过去时间时,浏览器会立即过期资源 - 但
max-age=0表达更清晰
Vary 响应头 是啥?
它解决了现代 Web 开发中资源多样性的核心问题:同一 URL 可能对应多个不同版本的资源。
核心作用机制
graph TB
A[客户端请求] --> B{服务器检查 Vary 指定头部}
B -->|Accept-Encoding: gzip| C[缓存版本A]
B -->|Accept-Encoding: br| D[缓存版本B]
B -->|Accept-Language: fr| E[缓存版本C]
C & D & E --> F[返回匹配的缓存副本]
Vary 的工作方式:
- 服务器声明响应内容依赖哪些请求头
- 缓存系统(浏览器/CDN)将这些头组合作为缓存键
- 后续请求必须完全匹配这些头的值才能使用缓存
关键使用场景
1. 内容压缩协商(最常用)
问题:同一资源有 gzip/brotli 等压缩版本
解决方案:
Vary: Accept-Encoding
缓存效果:
请求1: Accept-Encoding: gzip → 缓存版本A
请求2: Accept-Encoding: br → 缓存版本B
请求3: Accept-Encoding: gzip → 命中版本A
2. 多语言站点
问题:根据用户语言返回不同内容
解决方案:
Vary: Accept-Language
实际案例:
#请求头
Accept-Language: fr-FR
#响应头
Vary: Accept-Language
Content-Language: fr
3. 响应式图像服务
问题:根据设备 DPR 返回不同分辨率图像
解决方案:
Vary: DPR, Viewport-Width
现代替代方案:
<!-- 使用srcset避免Vary碎片化 -->
<img srcset="img-1x.jpg 1x, img-2x.jpg 2x">
4. 用户个性化内容
问题:登录用户看到不同内容
解决方案:
Vary: Cookie
风险警告:
- 这会使缓存完全失效(因为Cookie值唯一)
- 更好的替代方案:
Cache-Control: private
5. API 版本控制
问题:根据客户端版本返回不同数据结构
解决方案:
Vary: X-API-Version
请求示例:
GET /api/users
X-API-Version: 2.1
6. 客户端能力适配
问题:根据客户端支持特性返回不同资源
解决方案:
Vary: Save-Data, Device-Memory
现代实践:
// 通过客户端提示(Client Hints)优化
res.setHeader('Accept-CH', 'Device-Memory, DPR')
结论:何时使用 Vary
| 场景 | 推荐方案 | 缓存效率 |
|---|---|---|
| 内容压缩 | ✅ Vary: Accept-Encoding | 高 |
| 多语言 | ⚠️ 使用不同URL更好 | 中 |
| 用户个性化 | ❌ 避免使用Vary | 低 |
| 响应式图像 | ✅ 客户端提示+srcset | 高 |
| API版本控制 | ✅ URL版本化 /v2/resource | 极高 |
黄金法则:
只在真正影响资源内容且取值有限(如压缩格式、语言)时使用 Vary。对于用户级个性化,采用 Cache-Control: private 更有效。现代 Web 开发趋势是通过 URL 设计消除 Vary 需求,最大化缓存效率。
跨域资源共享 (CORS)
sequenceDiagram
participant Client as 浏览器
participant Server as 服务器
Note over Client: 1. 发起跨域请求
alt 简单请求(GET/POST/HEAD)
Client->>Server: GET /api/data<br>Origin: https://client.com
Server-->>Client: HTTP 200 OK<br>Access-Control-Allow-Origin: https://client.com<br>Data: {...}
else 复杂请求(PUT/DELETE等)
Note over Client: 2. 发送预检请求(OPTIONS)
Client->>Server: OPTIONS /api/data<br>Origin: https://client.com<br>Access-Control-Request-Method: DELETE<br>Access-Control-Request-Headers: X-API-Key
alt 预检通过
Server-->>Client: HTTP 204 No Content<br>Access-Control-Allow-Origin: https://client.com<br>Access-Control-Allow-Methods: DELETE<br>Access-Control-Allow-Headers: X-API-Key<br>Access-Control-Max-Age: 3600
Note over Client: 3. 发送实际请求
Client->>Server: DELETE /api/data<br>Origin: https://client.com<br>X-API-Key: 123abc
Server-->>Client: HTTP 200 OK<br>Access-Control-Allow-Origin: https://client.com<br>Data: {status: "deleted"}
else 预检失败
Server-->>Client: HTTP 403 Forbidden<br>(缺少必要CORS头)
Note over Client: 浏览器阻止实际请求
end
end
alt 需要携带凭证
Note over Client: 4. 客户端设置withCredentials
Client->>Server: GET /user<br>Origin: https://client.com<br>Cookie: sessionid=xyz
Server-->>Client: HTTP 200 OK<br>Access-Control-Allow-Origin: https://client.com<br>Access-Control-Allow-Credentials: true<br>Data: {user: "John"}
end
客户端请求头
- Origin:发起请求的源(协议+域名+端口)
Origin: https://www.client.com - Access-Control-Request-Method:
预检请求中声名实际请求的HTTP方法Access-Control-Request-Method: DELETE - Access-Control-Request-Headers:
预检请求中声名实际请求的自定义头Access-Control-Request-Headers: X-API-Key, Content-Type
服务端响应头
- Access-Control-Allow-Methods:允许的HTTP方法(
预检响应)Access-Control-Allow-Methods: GET, POST, PUT, DELETE - Access-Control-Allow-Origin:允许访问的源(必需)
Access-Control-Allow-Origin: https://www.client.com或*(禁止和凭证同用) - Access-Control-Allow-Headers:允许的自定义请求头(
预检响应)Access-Control-Allow-Headers: X-API-Key, Content-Type - Access-Control-Allow-Credentials:是否允许发送凭证(cookies/HTTP认证)
Access-Control-Allow-Credentials: true - Access-Control-Max-Age:
- Access-Control-Expose-Headers:允许客户端访问的响应头
Access-Control-Expose-Headers: X-Total-Count
完整交互流程
1. 简单请求流程(无需预检)
sequenceDiagram
participant Client as 浏览器
participant Server as 服务器
Client->>Server: GET /api/data<br>Origin: https://www.client.com
alt 允许跨域
Server-->>Client: HTTP 200 OK<br>Access-Control-Allow-Origin: https://www.client.com<br>Data: [...]
else 拒绝跨域
Server-->>Client: HTTP 200 OK (无CORS头)
Client-->>Error: 拦截响应
end
简单请求条件:
- 方法为:GET、HEAD、POST
- 头为:Accept、Accept-Language、Content-Language、Content-Type(仅限 application/x-www-form-urlencoded, multipart/form-data, text/plain)
2. 预检请求流程(复杂请求)
sequenceDiagram
participant Client as 浏览器
participant Server as 服务器
Note over Client: 检测到复杂请求
Client->>Server: OPTIONS /api/data<br>Origin: https://www.client.com<br>Access-Control-Request-Method: DELETE<br>Access-Control-Request-Headers: X-API-Key
alt 预检通过
Server-->>Client: HTTP 204 No Content<br>Access-Control-Allow-Origin: https://www.client.com<br>Access-Control-Allow-Methods: DELETE<br>Access-Control-Allow-Headers: X-API-Key<br>Access-Control-Max-Age: 3600
Client->>Server: DELETE /api/data<br>Origin: https://www.client.com<br>X-API-Key: 12345
Server-->>Client: HTTP 200 OK<br>Access-Control-Allow-Origin: https://www.client.com<br>Data: {...}
else 预检失败
Server-->>Client: HTTP 403 Forbidden (无CORS头或配置错误)
Client-->>Error: 阻止实际请求
end
触发预检的常见场景:
- 方法:PUT、DELETE、PATCH
- 自定义头:X-API-Key, Authorization
- Content-Type:application/json
凭证请求规则:
- 客户端设置:
fetch(url, { credentials: 'include' }) - 服务端必须:
- 设置
Access-Control-Allow-Credentials: true - 服务器不能将 Access-Control-Allow-Origin 的值设为通配符“*”,而应将其设置为特定的域,如:Access-Control-Allow-Origin: example.com。
- 服务器不能将 Access-Control-Allow-Headers 的值设为通配符“*”,而应将其设置为标头名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
- 服务器不能将 Access-Control-Allow-Methods 的值设为通配符“*”,而应将其设置为特定请求方法名称的列表,如:Access-Control-Allow-Methods: POST, GET
- 如果需要设置Cookie,必须添加
SameSite=None; Secure
- 设置