
为什么 HTTP 缓存很重要
HTTP 缓存是最高杠杆率的性能优化手段之一。一个正确缓存的资源无需服务器处理、无需数据库查询、无需传输任何字节——它直接从浏览器或 CDN 即时提供。
如果配置错误,缓存会导致用户在部署后数小时甚至数天内看到过时的内容。本指南涵盖每个重要的头部及其交互方式。

浏览器缓存如何工作
当浏览器收到响应时,它会根据头部决定是否缓存。在后续请求中,它首先检查缓存:
- 新鲜缓存命中: 直接使用缓存的响应——无需网络请求
- 过期缓存(需要重新验证): 向服务器发送条件请求
- 无缓存/已过期: 从服务器获取完整响应
Cache-Control:主要头部
Cache-Control 是主要的缓存指令。它取代了旧的 Pragma 和 Expires 头部。
常见指令
Cache-Control: max-age=3600
响应可在 3600 秒(1 小时)内使用,无需重新验证。
Cache-Control: no-cache
名称具有误导性。 并非“不要缓存”。而是“缓存它,但每次使用前必须重新验证”。浏览器存储响应,并在每次使用前发送条件请求(带 ETag 或 Last-Modified)。
Cache-Control: no-store
真正永不缓存。任何地方都不存储副本。用于包含敏感用户数据的响应。
Cache-Control: public
响应可由任何缓存(浏览器、CDN、代理)存储。默认用于没有凭据的响应。
Cache-Control: private
仅最终用户的浏览器应缓存此响应。防止 CDN 缓存用户特定的响应。
Cache-Control: immutable
告知浏览器此资源永不改变。即使用户手动刷新也跳过重新验证。仅用于内容哈希的资源。
Cache-Control: stale-while-revalidate=60
在后台获取更新的同时,最多提供 60 秒的过期内容。适用于可接受稍旧数据的 API。
Cache-Control: stale-if-error=86400
如果重新验证请求失败(服务器错误、网络超时),最多使用过期内容 86400 秒。提高弹性。
组合指令
Cache-Control: public, max-age=31536000, immutable
对于内容哈希的静态资源(文件名中包含哈希的 JS、CSS):永久缓存,无处不在,跳过重新验证。
Cache-Control: private, no-cache
对于用户特定的 HTML 页面:浏览器缓存但每次重新验证。
Cache-Control: no-store
对于银行对账单、医疗记录、敏感下载:永不缓存。
ETag:高效的重新验证
ETag(实体标签)是资源的版本标识符——通常是内容的哈希值或版本号。
流程:
- 服务器发送:
ETag: "abc123" - 浏览器缓存响应
- 下次请求,浏览器发送:
If-None-Match: "abc123" - 如果未更改:服务器返回
304 Not Modified(空主体,非常快) - 如果已更改:服务器返回
200 OK及新内容和新 ETag
// Express — ETag 中间件(内置,默认启用)
app.use(express.static('public')); // 自动提供 ETag
// 手动 ETag
const crypto = require('crypto');
app.get('/api/data', (req, res) => {
const data = getDataFromDB();
const etag = '"' + crypto.createHash('md5').update(JSON.stringify(data)).digest('hex') + '"';
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'private, no-cache'); // 每次重新验证
res.json(data);
});
强 ETag 与弱 ETag
强 ETag("abc123"):逐字节相同的内容。如果响应因内容编码而异则不能使用。
弱 ETag(W/"abc123"):语义等价但可能在细微方面不同(例如,gzip 压缩与未压缩)。更灵活。

Last-Modified:传统方法
在 ETag 之前,Last-Modified 用于重新验证。
响应: Last-Modified: Mon, 26 May 2026 10:00:00 GMT
请求: If-Modified-Since: Mon, 26 May 2026 10:00:00 GMT
响应: 304 Not Modified
ETag 更受青睐,因为时间戳只有 1 秒分辨率(同一秒内更新的文件会导致问题),并且无法捕获不更新文件系统时间戳的内容更改。
许多服务器同时发送两者——ETag 优先。
Vary 头部
Vary 告知缓存不同的请求头部会产生不同的响应。如果缓存存储了一个版本,它只能用于具有匹配头部值的请求。
Vary: Accept-Encoding
为 gzip、br 和 identity 编码的客户端缓存单独的版本。
Vary: Accept-Language
为每种语言缓存单独的版本。适用于国际化内容。
Vary: Authorization
⚠️ 不要这样做。它会为每个不同的 Authorization 值创建单独的缓存条目——实际上禁用了缓存。
按资源类型的缓存策略
文件名包含哈希的静态资源
main.abc123.js, styles.def456.css
Cache-Control: public, max-age=31536000, immutable
缓存 1 年,可被 CDN 缓存,永不重新验证。当内容更改时,文件名中的哈希也会更改,因此浏览器始终获取新文件。这就是缓存破坏。
HTML 页面
Cache-Control: no-cache
始终重新验证。HTML 页面引用哈希资源——保持 HTML 新鲜可确保用户获得最新的资源文件名。
API 响应(用户特定)
Cache-Control: private, no-cache
配合 ETag:浏览器缓存并重新验证,如果数据未更改则节省带宽。

API 响应(公共,更新不频繁)
Cache-Control: public, max-age=300, stale-while-revalidate=30
缓存 5 分钟,在后台刷新时最多再提供 30 秒过期内容。
敏感数据
Cache-Control: no-store
银行、医疗、个人身份信息——绝不存储副本。
配置缓存头部
Nginx
# 哈希资源的长期缓存
location ~* .(js|css|woff2|png|jpg|svg)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# HTML 的短缓存
location ~* .html$ {
add_header Cache-Control "no-cache";
}
# API 响应——无 CDN 缓存,浏览器重新验证
location /api/ {
add_header Cache-Control "private, no-cache";
add_header Vary "Authorization";
}
Express
const express = require('express');
const path = require('path');
const app = express();
// 带哈希的静态资源——不可变缓存
app.use('/assets', express.static(path.join(__dirname, 'public/assets'), {
maxAge: '1y',
immutable: true,
}));
// HTML 页面——始终重新验证
app.use(express.static(path.join(__dirname, 'public'), {
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) {
res.setHeader('Cache-Control', 'no-cache');
}
},
}));
// API 路由——私有,重新验证
app.get('/api/*', (req, res, next) => {
res.setHeader('Cache-Control', 'private, no-cache');
next();
});
缓存破坏策略
文件名哈希(推荐):在文件名中包含内容哈希。Vite、webpack 和大多数打包工具会自动执行此操作。
之前: /assets/main.js
之后: /assets/main.Bx9k2Ar7.js ← 内容更改时哈希变化
查询字符串版本化: 附加版本参数。效果较差——某些缓存忽略查询字符串。
<script src="/assets/main.js?v=2.1.3"></script>
URL 版本化: 在路径中包含版本。可靠但需要在每次发布时更新 HTML。
/v2/assets/main.js
快速参考
| 资源类型 | 推荐的 Cache-Control |
|---|---|
| 内容哈希的 JS/CSS | public, max-age=31536000, immutable |
| 图片/字体(哈希) | public, max-age=31536000, immutable |
| HTML 页面 | no-cache |
| API(公共数据) | public, max-age=300, stale-while-revalidate=30 |
| API(用户数据) | private, no-cache |
| 敏感数据 | no-store |
→ 使用 HTTP 状态码 参考了解 304 Not Modified 及其他与缓存相关的响应。