根据 HTTP Archive 2026 年 5 月的数据,全球 Top 10000 网站中,资源加载时间占页面总加载时间的 68%,而正确配置 HTTP 缓存和 CDN 的网站,首屏加载时间平均快 2.4 秒,带宽成本降低 40-60%。然而,大量开发者的缓存策略仍停留在「全部设成 no-cache」或「全部设成一年过期」的极端状态——前者浪费了缓存的全部价值,后者在部署新版本时制造了无数线上事故。如果你正在为 Web 应用的加载速度、CDN 成本或缓存一致性头疼,这篇文章会给你一套从原理到生产级配置的完整方案。
🚀 一、HTTP 缓存机制深度解析
HTTP 缓存是 Web 性能优化中投入产出比最高的手段——不需要改代码、不需要引入新框架,只需要正确配置 HTTP 头部,就能让重复访问的加载时间从秒级降到毫秒级。但大多数开发者对缓存的理解停留在「设置一个过期时间」,对 Cache-Control 指令的组合、ETag 的生成策略、以及强缓存与协商缓存的协作机制知之甚少。
1.1 强缓存 vs 协商缓存:两道防线
HTTP 缓存分为两道防线:
- ✅ 强缓存(Strong Cache):浏览器直接使用本地缓存,不发请求到服务器。命中时 HTTP 状态码显示
200 (from disk cache)或200 (from memory cache)。 - ✅ 协商缓存(Negotiation Cache):浏览器向服务器确认资源是否更新。如果未更新,服务器返回
304 Not Modified,浏览器使用本地缓存;如果已更新,返回新资源。
📌 **记住:**强缓存的优先级永远高于协商缓存。只有强缓存未命中(过期)时,浏览器才会发起协商缓存请求。设计缓存策略时,核心决策是「哪些资源走强缓存,哪些走协商缓存」。
1.2 Cache-Control 指令完全指南
Cache-Control 是 HTTP/1.1 引入的缓存控制头部,取代了 Expires 和 Pragma。以下是生产环境中最常用的指令:
| 指令 | 含义 | 适用场景 | 推荐 |
|---|---|---|---|
max-age=N |
资源在 N 秒内有效 | 静态资源(JS/CSS/图片) | ✅ 推荐 |
no-cache |
每次使用前必须向服务器验证 | HTML 入口文件 | ✅ 推荐 |
no-store |
完全不缓存 | 敏感数据(支付、个人信息) | ⚠️ 慎用 |
public |
允许 CDN 等中间代理缓存 | 静态资源 | ✅ 推荐 |
private |
只允许浏览器缓存 | 用户个性化内容 | ✅ 推荐 |
immutable |
资源永远不会改变 | 带哈希的静态资源 | ✅ 推荐 |
stale-while-revalidate=N |
过期后 N 秒内可先用旧值,后台更新 | API 响应、频繁访问资源 | ✅ 推荐 |
⚠️ **警告:**永远不要对 HTML 入口文件设置
max-age。如果你的index.html被强缓存了,用户在你发布新版本后看到的仍然是旧的 JS/CSS 引用——即使新的 JS/CSS 文件已经部署,HTML 里引用的还是旧文件名。
一个生产级的缓存策略配置如下:
# Nginx 缓存配置 — 不同资源类型的缓存策略
# 位置:/etc/nginx/conf.d/cache.conf
# HTML 入口文件:不缓存,每次都协商验证
location ~* \.html$ {
add_header Cache-Control "no-cache, must-revalidate";
add_header ETag ""; # HTML 不需要 ETag,用 Last-Modified 即可
}
# 带哈希的静态资源(JS/CSS/图片):强缓存一年 + immutable
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
# immutable 告诉浏览器:这个资源永远不会变,别发协商请求
}
# API 响应:不缓存或短时间缓存
location /api/ {
add_header Cache-Control "private, no-store";
# 敏感 API 完全不缓存
}
# 字体文件:长期缓存 + 允许跨域
location ~* \.(woff|woff2|ttf|otf)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
}
1.3 ETag vs Last-Modified:协商缓存的选择
协商缓存有两种机制,它们的精度和性能有显著差异:
| 对比维度 | ETag | Last-Modified |
|---|---|---|
| 精度 | 文件内容的哈希值,精确到字节级 | 文件修改时间,精度到秒级 |
| 性能 | 服务器需要计算哈希(CPU 开销) | 只读取文件元数据(几乎零开销) |
| 适用场景 | 频繁修改且内容变化小的资源 | 修改频率低的静态资源 |
| 分布式一致性 | ✅ 每个节点生成相同哈希即可 | ⚠️ 不同服务器的文件时间可能不一致 |
| 推荐度 | ✅ 推荐(精度高) | ⚠️ 仅用于 HTML 入口文件 |
// Node.js (Express) — 生成 ETag 的生产级实现
import { createHash } from 'crypto';
import { readFileSync, statSync } from 'fs';
function generateETag(filePath) {
const content = readFileSync(filePath);
const stat = statSync(filePath);
// 组合文件大小和内容哈希,避免大文件全量计算
const hash = createHash('md5')
.update(`${stat.size}-${stat.mtimeMs}`)
.digest('hex')
.slice(0, 16); // 截取 16 位足够避免冲突
return `"${hash}"`;
}
app.get('/static/:file', (req, res) => {
const filePath = `./public/${req.params.file}`;
const etag = generateETag(filePath);
// 协商缓存:If-None-Match 匹配 ETag
if (req.headers['if-none-match'] === etag) {
return res.status(304).end(); // 资源未变化,不返回内容
}
res.set({
'ETag': etag,
'Cache-Control': 'public, max-age=3600', // 强缓存 1 小时
});
res.sendFile(filePath);
});
💡 **提示:**在 Nginx 中,ETag 默认由
last modified time+content length组成。如果你在多台服务器间部署,确保使用共享存储(如 NFS)或统一的构建流程,否则不同服务器生成的 ETag 可能不一致,导致缓存失效。
🔧 二、CDN 架构与生产级配置
CDN(Content Delivery Network,内容分发网络)的本质是将你的静态资源缓存到离用户最近的边缘节点。用户请求资源时,不再回源到你的服务器,而是由最近的 CDN 节点直接响应——将网络延迟从 200-500ms 降低到 10-50ms。
2.1 CDN 工作原理与缓存层级
一个完整的 CDN 请求链路包含多个缓存层级:
用户浏览器缓存 → CDN 边缘节点(Edge)→ CDN 区域中心(Regional)→ 源站(Origin)
每一层都可以独立配置缓存策略。关键点在于:
- ✅ 浏览器缓存:由
Cache-Control头部控制,是最快的缓存层(0ms) - ✅ CDN 边缘节点:由 CDN 配置控制,延迟 10-50ms
- ✅ CDN 区域中心:多个边缘节点共享的缓存层,延迟 50-100ms
- ✅ 源站:你的服务器,延迟 100-500ms
⚡ **关键结论:**CDN 的核心价值不仅在于「加速」,更在于「减负」。一个正确配置 CDN 的网站,90% 以上的静态资源请求不会到达源站,这直接降低了服务器的带宽成本和负载压力。
2.2 主流 CDN 供应商对比
选择 CDN 时需要考虑覆盖范围、性能、价格和附加功能:
| 对比维度 | Cloudflare | AWS CloudFront | 阿里云 CDN | 腾讯云 CDN |
|---|---|---|---|---|
| 全球节点数 | 330+ | 600+ | 3200+(含中国) | 2800+(含中国) |
| 免费额度 | 无限带宽(免费计划) | 1TB/月(免费计划) | 无免费 | 无免费 |
| 边缘计算 | Workers(强大) | Lambda@Edge | 函数计算 | 云函数 |
| DDoS 防护 | ✅ 免费基础防护 | 需额外购买 WAF | ✅ 基础防护 | ✅ 基础防护 |
| 中国大陆加速 | ❌ 需企业计划 | ❌ 需 ICP 备案 | ✅ 原生支持 | ✅ 原生支持 |
| 起步价格 | $0(免费计划) | $0.085/GB | ¥0.24/GB | ¥0.21/GB |
| 推荐场景 | 全球用户、个人项目 | AWS 生态用户 | 中国用户为主 | 中国用户为主 |
💡 **提示:**如果你的目标用户主要在中国大陆,必须选择阿里云或腾讯云 CDN——Cloudflare 和 CloudFront 在中国的节点有限,延迟优势不明显。同时,使用国内 CDN 需要域名完成 ICP 备案。
2.3 Cloudflare Workers 边缘缓存实战
Cloudflare Workers 让你可以在 CDN 边缘节点运行自定义逻辑——这是传统 CDN 做不到的。以下是一个生产级的边缘缓存 + 动态路由示例:
// Cloudflare Workers — 边缘缓存 + API 路由
// wrangler.toml: name = "edge-cache-demo", main = "src/index.js"
export default {
async fetch(request, env) {
const url = new URL(request.url);
const cache = caches.default;
// 1. 静态资源走 CDN 缓存(Cache API)
if (url.pathname.match(/\.(js|css|png|jpg|woff2)$/)) {
const cacheKey = new Request(url.toString(), request);
let response = await cache.match(cacheKey);
if (!response) {
// 缓存未命中,从源站获取
response = await fetch(request);
// 克隆响应(因为 body 只能读取一次)
const cachedResponse = new Response(response.body, response);
cachedResponse.headers.set('Cache-Control', 'public, max-age=31536000');
// 写入 Cache API
await cache.put(cacheKey, cachedResponse.clone());
return cachedResponse;
}
return response;
}
// 2. API 请求:在边缘添加 CORS 头 + 速率限制
if (url.pathname.startsWith('/api/')) {
// 简单的速率限制(基于 IP)
const ip = request.headers.get('cf-connecting-ip');
const rateLimitKey = `rate:${ip}`;
const current = await env.KV.get(rateLimitKey);
if (current && parseInt(current) > 100) {
return new Response('Rate limit exceeded', { status: 429 });
}
// 递增计数(设置 60 秒过期)
await env.KV.put(rateLimitKey, String((parseInt(current) || 0) + 1), {
expirationTtl: 60,
});
// 回源请求
const response = await fetch(request);
const newResponse = new Response(response.body, response);
newResponse.headers.set('Access-Control-Allow-Origin', '*');
newResponse.headers.set('Cache-Control', 'private, no-store');
return newResponse;
}
// 3. HTML 页面:边缘渲染 + 短时间缓存
const cacheKey = new Request(url.toString(), request);
let response = await cache.match(cacheKey);
if (!response) {
response = await fetch(request);
const cachedResponse = new Response(response.body, response);
cachedResponse.headers.set('Cache-Control', 'public, s-maxage=60, stale-while-revalidate=300');
await cache.put(cacheKey, cachedResponse.clone());
return cachedResponse;
}
return response;
},
};
⚠️ 警告:
stale-while-revalidate是一个被严重低估的指令。它允许 CDN 在资源过期后的 N 秒内先返回旧值,同时在后台异步更新。对于频繁访问的页面(如首页),这可以将 P99 延迟降低 80%,同时保证数据在可接受的时间窗口内更新。
💡 三、高级缓存策略与避坑指南
3.1 缓存三大问题:击穿、穿透、雪崩
这三个概念来自服务端缓存(Redis),但在 CDN 场景中同样存在:
| 问题 | 描述 | CDN 场景表现 | 解决方案 |
|---|---|---|---|
| 缓存击穿 | 热点资源突然过期,大量请求同时回源 | CDN 节点缓存过期,并发请求打到源站 | stale-while-revalidate + 源站限流 |
| 缓存穿透 | 请求的资源在源站也不存在 | 恶意请求不存在的 URL,每次都回源 | CDN 层返回 404 并缓存(短时间) |
| 缓存雪崩 | 大量资源同时过期 | 部署时所有资源 URL 变化,CDN 全部回源 | 资源版本化 + 渐进式预热 |
// 版本化资源的缓存策略 — 前端构建配置
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
// 文件名包含内容哈希:app.a1b2c3d4.js
rollupOptions: {
output: {
// JS/CSS 使用内容哈希 — 内容变了文件名才变
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
assetFileNames: 'assets/[name].[hash].[ext]',
},
},
},
// HTML 不使用哈希 — 文件名不变,通过 no-cache 保证最新
// index.html 始终是 index.html,由 Nginx 的 no-cache 策略控制
});
📌 记住:版本化资源(带哈希的文件名)是解决缓存一致性的终极方案。当 JS/CSS 文件名包含内容哈希时,内容变化 = 文件名变化 = 新的缓存条目。配合
max-age=31536000, immutable,浏览器永远不会发协商请求,性能最优。
3.2 N+1 缓存失效:部署时的缓存一致性
部署新版本时最常见的问题是:新的 HTML 已经上线,但引用的还是旧的 JS/CSS 文件名;或者新的 JS/CSS 已经上线,但 HTML 还是旧的缓存版本。
解决方案是两阶段部署:
#!/bin/bash
# 两阶段部署脚本 — 确保缓存一致性
# 第一阶段:上传新的静态资源(JS/CSS/图片)
# 这些资源的文件名包含哈希,不会与旧版本冲突
aws s3 sync ./dist/assets s3://my-bucket/assets \
--cache-control "public, max-age=31536000, immutable" \
--delete # 删除旧版本的文件
# 等待 CDN 边缘节点缓存新资源(可选:主动预热)
echo "等待 CDN 缓存新资源..."
sleep 10
# 第二阶段:上传新的 HTML(覆盖旧文件)
# HTML 文件使用 no-cache,用户每次访问都会获取最新版本
aws s3 sync ./dist s3://my-bucket \
--exclude "assets/*" \
--cache-control "no-cache, must-revalidate"
# 清除 CDN 边缘节点的 HTML 缓存
# Cloudflare API
curl -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"files":["https://example.com/","https://example.com/index.html"]}'
3.3 Service Worker 缓存策略
Service Worker 提供了比 HTTP 缓存更精细的控制能力。以下是一个生产级的 Workbox 缓存策略:
// service-worker.js — 使用 Workbox 实现多层缓存策略
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { CacheableResponsePlugin } from 'workbox-cacheable-response';
import { ExpirationPlugin } from 'workbox-expiration';
// 1. 静态资源:Cache First(优先缓存,缓存未命中才回源)
registerRoute(
({ request }) => ['style', 'script', 'image', 'font'].includes(request.destination),
new CacheFirst({
cacheName: 'static-assets-v1',
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxEntries: 500, maxAgeSeconds: 30 * 24 * 60 * 60 }),
],
})
);
// 2. API 请求:Network First(优先网络,网络失败才用缓存)
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache-v1',
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxEntries: 100, maxAgeSeconds: 5 * 60 }),
],
networkTimeoutSeconds: 3, // 3 秒超时后降级到缓存
})
);
// 3. HTML 页面:Stale While Revalidate(先返回缓存,后台更新)
registerRoute(
({ request }) => request.mode === 'navigate',
new StaleWhileRevalidate({
cacheName: 'pages-v1',
plugins: [
new CacheableResponsePlugin({ statuses: [200] }),
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 24 * 60 * 60 }),
],
})
);
⚡ 关键结论:Service Worker 缓存是 HTTP 缓存和 CDN 缓存之上的第三层防线。它的核心价值在于:即使用户断网,仍然可以展示缓存的页面和资源。对于 PWA 应用,Service Worker 缓存是离线体验的基石。
3.4 缓存监控与调试
在生产环境中,你需要监控缓存命中率来验证配置是否正确:
// 缓存监控中间件 — 记录缓存命中率
// 适用于 Express / Hono / Fastify 等框架
function cacheMonitor() {
const stats = { hit: 0, miss: 0, bypass: 0 };
return (req, res, next) => {
const startTime = Date.now();
// 监听响应完成事件
res.on('finish', () => {
const cacheStatus = res.getHeader('X-Cache-Status') || 'UNKNOWN';
const duration = Date.now() - startTime;
if (cacheStatus === 'HIT') stats.hit++;
else if (cacheStatus === 'MISS') stats.miss++;
else stats.bypass++;
// 每 1000 次请求输出统计
const total = stats.hit + stats.miss + stats.bypass;
if (total % 1000 === 0) {
const hitRate = ((stats.hit / total) * 100).toFixed(1);
console.log(`[Cache] 命中率: ${hitRate}% | HIT: ${stats.hit} | MISS: ${stats.miss} | BYPASS: ${stats.bypass}`);
}
});
next();
};
}
在 Chrome DevTools 中调试缓存:打开 Network 面板 → 右键点击列头 → 勾选 Cache-Control 和 Size 列。from disk cache 和 from memory cache 表示命中了强缓存,304 表示命中了协商缓存。
⚠️ 四、常见坑点与避坑指南
以下是生产环境中最常见的缓存问题:
- ❌ HTML 使用
max-age强缓存 → 新版本发布后用户看到旧页面 - ❌ 静态资源不带哈希 → 无法设置长期缓存,每次部署都要清 CDN
- ❌ CDN 缓存了
Set-Cookie头 → 用户数据泄露(CDN 返回其他用户的 Cookie) - ❌ API 响应被 CDN 缓存 → 用户看到其他用户的数据
- ❌
Cache-Control和Expires同时设置且值不一致 → 浏览器行为不可预测 - ❌ CDN 回源时携带
Cookie→ 源站负载不降反增(每次请求都带 Cookie)
⚠️ 警告:CDN 默认会缓存所有响应,包括
Set-Cookie头部。如果你的 API 响应包含了Set-Cookie,CDN 会将这个 Cookie 缓存下来,后续请求的用户会收到别人的 Cookie——这是一个严重的安全漏洞。务必在 CDN 配置中剥离Set-Cookie头部,或对 API 路径设置Cache-Control: private, no-store。
📊 五、缓存策略决策流程
根据资源类型选择正确的缓存策略:
| 资源类型 | Cache-Control | CDN 配置 | Service Worker | 示例 |
|---|---|---|---|---|
| 带哈希的 JS/CSS | max-age=31536000, immutable |
长期缓存 | Cache First | app.a1b2c3.js |
| HTML 入口文件 | no-cache, must-revalidate |
短期缓存或不缓存 | Network First | index.html |
| API 响应 | private, no-store |
不缓存 | Network First | /api/user |
| 用户头像 | private, max-age=86400 |
不缓存 | Stale While Revalidate | /avatar.png |
| 公共图片 | public, max-age=604800 |
长期缓存 | Cache First | /logo.svg |
| 字体文件 | public, max-age=31536000, immutable |
长期缓存 + CORS | Cache First | /font.woff2 |
🎯 总结
HTTP 缓存和 CDN 是 Web 性能优化中成本最低、收益最高的手段。核心原则只有三条:
- ✅ 版本化资源长期缓存:带哈希的 JS/CSS/图片设置
max-age=31536000, immutable,永远不发协商请求 - ✅ HTML 入口不缓存:
index.html使用no-cache,确保用户始终获取最新的资源引用 - ✅ 敏感数据不经过 CDN:API 响应和用户数据使用
private, no-store
相关工具推荐:
- 🔧 jsjson.com JSON 格式化工具 — 格式化 API 响应数据,验证缓存是否正确返回
- 🔧 jsjson.com 在线 Base64 编码 — 编码 CDN 配置文件中的特殊字符
- 🔧 jsjson.com 正则表达式测试 — 测试 Nginx 缓存规则中的正则匹配