引言:HTTP缓存的重要性
HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和中间代理服务器中存储资源副本,显著减少网络请求、降低服务器负载并提升用户体验。一个正确配置的缓存策略可以将页面加载时间减少50%以上,同时大幅降低带宽成本。
缓存带来的核心优势
- 减少延迟:避免重复下载相同资源
- 降低服务器负载:减少不必要的请求处理
- 提升用户体验:更快的页面响应速度
- 节省带宽:减少网络传输数据量
HTTP缓存基础概念
缓存的分类
HTTP缓存主要分为两类:
- 强缓存(Strong Caching):浏览器直接从缓存中读取资源,无需与服务器通信
- 协商缓存(Negotiated Caching):浏览器需要向服务器验证资源是否过期
缓存流程概览
浏览器请求 → 检查缓存 → 强缓存有效? → 是 → 直接使用缓存
↓ 否
发送请求到服务器 → 协商缓存验证 → 资源未修改? → 是 → 304 Not Modified
↓ 否
200 OK + 新资源
强缓存策略详解
强缓存通过HTTP响应头控制,当缓存有效时,浏览器不会发送任何网络请求。
Cache-Control头部
Cache-Control是HTTP/1.1中最重要的缓存控制头部,它提供了精细的缓存控制能力。
常用指令详解
max-age=seconds 指定资源的最大缓存时间(秒)。例如:
Cache-Control: max-age=3600
表示资源可以缓存1小时。
no-cache 注意:这个名称容易误解。它不是不缓存,而是必须先与服务器验证缓存有效性。
Cache-Control: no-cache
no-store 真正意义上的不缓存,浏览器和中间缓存都不应存储资源副本。
Cache-Control: no-store
must-revalidate 当缓存过期后,必须重新验证,不能使用过期的缓存。
Cache-Control: must-revalidate
public vs private
public:资源可以被任何缓存存储(包括CDN、代理服务器)private:资源只能被用户浏览器缓存,不能被CDN等中间缓存存储
Expires头部(HTTP/1.0)
Expires是HTTP/1.0的遗留头部,指定资源的过期时间(GMT格式)。
Expires: Thu, 31 Dec 2025 23:59:59 GMT
注意:Cache-Control的max-age优先级更高。建议同时设置两者以实现向后兼容。
实际应用示例
静态资源缓存策略
# 静态资源(如JS/CSS/图片)- 长期缓存
Cache-Control: public, max-age=31536000, immutable
Expires: Thu, 31 Dec 2025 23:59:59 GMT
# HTML文档 - 短期缓存或不缓存
Cache-Control: no-cache
协商缓存策略详解
当强缓存过期或未设置时,浏览器会发起请求,通过协商缓存头与服务器确认资源是否需要更新。
基于时间的验证:If-Modified-Since
工作原理:
- 服务器首次响应时发送
Last-Modified头部 - 浏览器下次请求时发送
If-Modified-Since头部 - 服务器比较时间戳,决定返回304还是200
服务器端实现示例(Node.js):
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer((req, res) => {
const filePath = path.join(__dirname, 'static', req.url);
fs.stat(filePath, (err, stats) => {
if (err) {
res.writeHead(404);
res.end('Not Found');
return;
}
const lastModified = stats.mtime.toUTCString();
const ifModifiedSince = req.headers['if-modified-since'];
// 设置Last-Modified头部
res.setHeader('Last-Modified', lastModified);
// 比较时间戳
if (ifModifiedSince === lastModified) {
res.writeHead(304); // 未修改
res.end();
} else {
// 返回新资源
res.writeHead(200, {
'Content-Type': 'text/plain',
'Cache-Control': 'no-cache'
});
fs.createReadStream(filePath).pipe(res);
}
});
});
server.listen(3000);
基于内容的验证:ETag
ETag(Entity Tag)是资源的唯一标识符,通常基于内容的哈希值或版本号。
工作原理:
- 服务器首次响应时发送
ETag头部 - 浏览器下次请求时发送
If-None-Match头部 - 服务器比较ETag值,决定返回304还是200
ETag生成示例(Node.js):
const crypto = require('crypto');
const fs = require('fs');
function generateETag(content) {
return crypto.createHash('md5').update(content).digest('hex');
}
// 在响应中设置ETag
const content = fs.readFileSync('file.js');
const etag = generateETag(content);
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'no-cache');
// 验证请求
const clientEtag = req.headers['if-none-match'];
if (clientEtag === etag) {
res.writeHead(304);
res.end();
} else {
res.writeHead(200);
res.end(content);
}
ETag vs Last-Modified 对比
| 特性 | ETag | Last-Modified |
|---|---|---|
| 精确度 | 基于内容,精确 | 基于时间,可能因文件属性修改而变化 |
| 性能 | 需要计算哈希,消耗CPU | 只需读取文件元数据 |
| 可靠性 | 更可靠 | 可能因服务器时间不同步而失效 |
| 使用场景 | 频繁修改的资源 | 静态资源 |
缓存策略最佳实践
1. 资源分类缓存策略
HTML文档
Cache-Control: no-cache
# 或
Cache-Control: max-age=0, must-revalidate
HTML通常包含动态内容,应设置较短的缓存时间或每次验证。
静态资源(JS/CSS/图片)
# 长期缓存(1年)
Cache-Control: public, max-age=31536000, immutable
# 文件名带哈希值时(如app.a1b2c3.js)
# 可以安全地长期缓存
API响应
# 动态API - 短期缓存
Cache-Control: private, max-age=60
# 不变的API数据
Cache-Control: public, max-age=3600
2. 版本控制与文件哈希
Webpack配置示例:
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
// 自动注入带哈希的资源
filename: 'index.html'
})
]
};
构建后的文件名:
app.a1b2c3d4.js
vendor.e5f6g7h8.js
这样配置后,可以安全地设置长期缓存,因为文件名变化会强制浏览器重新下载。
3. 使用Service Worker进行精细控制
Service Worker提供了程序化的缓存控制能力:
// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png'
];
// 安装阶段:预缓存资源
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
// 拦截请求并返回缓存
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中
if (response) {
return response;
}
// 缓存未命中,发起网络请求
return fetch(event.request).then(response => {
// 检查响应是否有效
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 克隆响应(响应流只能使用一次)
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
// 只缓存GET请求
if (event.request.method === 'GET') {
cache.put(event.request, responseToCache);
}
});
return response;
});
})
);
});
// 清理旧缓存
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
4. CDN缓存配置
Nginx配置示例:
# 静态资源 - 长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header X-Cache-Status $upstream_cache_status;
# 开启gzip压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
# HTML文档 - 不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
}
# API接口 - 短期缓存
location /api/ {
expires 60s;
add_header Cache-Control "private, max-age=60";
}
常见问题与解决方案
问题1:缓存过期导致用户看到旧内容
场景:用户访问网站,看到旧版本的CSS或JS,导致页面显示异常。
解决方案:
方案A:文件名哈希(推荐)
// 构建配置
output: {
filename: '[name].[contenthash:8].js'
}
方案B:版本号查询参数
<!-- 在HTML中引入资源时添加版本号 -->
<script src="/app.js?v=1.2.3"></script>
方案C:清除浏览器缓存
Cache-Control: no-cache, must-revalidate
问题2:缓存穿透
场景:大量请求访问不存在的资源,每次都穿透缓存到数据库。
解决方案:
缓存空结果:
// Redis缓存示例
async function getFromCache(key) {
const cached = await redis.get(key);
if (cached !== null) {
return JSON.parse(cached);
}
// 查询数据库
const data = await db.query(key);
if (data) {
// 缓存真实数据
await redis.setex(key, 3600, JSON.stringify(data));
return data;
} else {
// 缓存空结果(设置较短过期时间)
await redis.setex(key, 60, JSON.stringify({ notFound: true }));
return null;
}
}
问题3:缓存雪崩
场景:大量缓存同时过期,导致瞬时请求压垮数据库。
解决方案:
随机过期时间:
// 设置缓存时添加随机值
function setCacheWithJitter(key, data, baseTTL) {
// 添加±10%的随机时间
const jitter = baseTTL * 0.1 * (Math.random() - 0.5);
const ttl = Math.floor(baseTTL + jitter);
redis.setex(key, ttl, JSON.stringify(data));
}
多级缓存策略:
// 本地内存缓存 + Redis缓存
const localCache = new Map();
async function getData(key) {
// 1. 检查本地缓存
if (localCache.has(key)) {
const item = localCache.get(key);
if (item.expiry > Date.now()) {
return item.data;
}
localCache.delete(key);
}
// 2. 检查Redis
const redisData = await redis.get(key);
if (redisData) {
// 回填本地缓存
localCache.set(key, {
data: JSON.parse(redisData),
expiry: Date.now() + 30000 // 30秒
});
return JSON.parse(redisData);
}
// 3. 查询数据库
const dbData = await db.query(key);
if (dbData) {
// 同时写入两级缓存
await redis.setex(key, 3600, JSON.stringify(dbData));
localCache.set(key, {
data: dbData,
expiry: Date.now() + 30000
});
return dbData;
}
return null;
}
问题4:移动端缓存问题
场景:移动端浏览器缓存策略不一致,导致更新后用户仍看到旧内容。
解决方案:
移动端专用配置:
# 移动端HTML
Cache-Control: no-cache, must-revalidate, max-age=0
# 移动端静态资源
Cache-Control: public, max-age=86400 # 24小时,比桌面端短
检测移动端并动态设置:
// Express中间件
app.use((req, res, next) => {
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(req.headers['user-agent']);
if (isMobile) {
// 移动端使用更保守的缓存策略
res.setHeader('Cache-Control', 'public, max-age=3600');
} else {
// 桌面端使用长期缓存
res.setHeader('Cache-Control', 'public, max-age=31536000');
}
next();
});
缓存监控与调试
使用Chrome DevTools调试缓存
Network面板:
- 查看Size列:
(memory cache)、(disk cache)表示命中缓存 - 查看Status列:304表示协商缓存命中
- 查看Time列:缓存命中请求通常<10ms
- 查看Size列:
Application面板:
- 查看Cache Storage中的Service Worker缓存
- 查看HTTP缓存的使用情况
缓存命中率监控
Node.js监控示例:
// Express中间件:记录缓存命中率
const cacheStats = {
hits: 0,
misses: 0,
total: 0
};
app.use((req, res, next) => {
const originalSend = res.send;
res.send = function(body) {
const cacheStatus = res.getHeader('X-Cache-Status');
if (cacheStatus === 'HIT') {
cacheStats.hits++;
} else if (cacheStatus === 'MISS') {
cacheStats.misses++;
}
cacheStats.total++;
// 每1000个请求打印统计
if (cacheStats.total % 1000 === 0) {
const hitRate = (cacheStats.hits / cacheStats.total * 100).toFixed(2);
console.log(`Cache Hit Rate: ${hitRate}% (${cacheStats.hits}/${cacheStats.total})`);
}
originalSend.call(this, body);
};
next();
});
// 缓存状态中间件
app.use((req, res, next) => {
const cacheHeader = req.headers['x-cache-status'];
if (cacheHeader === 'HIT') {
res.setHeader('X-Cache-Status', 'HIT');
} else {
res.setHeader('X-Cache-Status', 'MISS');
}
next();
});
使用WebPageTest进行性能测试
配置缓存测试:
// WebPageTest脚本
{
"tests": [
{
"label": "Cache Performance",
"url": "https://your-site.com",
"firstViewOnly": false,
"repeatView": true,
"cache": true
}
]
}
高级缓存策略
1. Vary头部的使用
Vary头部用于指定哪些请求头影响缓存的变体。
# 根据User-Agent返回不同内容
Vary: User-Agent
# 根据Accept-Encoding返回不同内容(压缩)
Vary: Accept-Encoding
# 多个请求头
Vary: User-Agent, Accept-Language
注意:谨慎使用Vary: *,这会完全禁用缓存。
2. 预加载和预取
资源预加载:
<!-- 在HTML中预加载关键资源 -->
<link rel="preload" href="/critical.css" as="style">
<link rel="preload" href="/app.js" as="script">
<link rel="preload" href="/hero-image.jpg" as="image">
DNS预取:
<link rel="dns-prefetch" href="//cdn.example.com">
3. 缓存破坏(Cache Busting)策略
文件名哈希(最佳实践):
app.a1b2c3d4.js → 内容变化 → app.e5f6g7h8.js
查询参数(次选):
<script src="/app.js?v=1.2.3"></script>
目录版本:
<script src="/v1.2.3/app.js"></script>
性能测试与优化
缓存性能指标
关键指标:
- 缓存命中率:>90%为优秀
- 平均响应时间:缓存命中应<50ms
- 带宽节省:应减少60-80%的重复数据传输
使用Lighthouse审计缓存
Lighthouse缓存审计规则:
{
"cache-policy": {
"maxAge": 86400,
"mustRevalidate": false
}
}
缓存优化检查清单
- [ ] 静态资源是否设置了长期缓存?
- [ ] HTML文档是否设置了合理的缓存策略?
- [ ] 是否使用文件名哈希避免缓存问题?
- [ ] 是否启用了CDN缓存?
- [ ] 是否监控缓存命中率?
- [ ] 是否处理了缓存穿透和雪崩?
- [ ] 是否使用了Service Worker进行精细控制?
- [ ] 是否测试了移动端缓存行为?
总结
HTTP缓存是提升网站性能的关键技术。通过合理配置Cache-Control、ETag等头部,结合文件名哈希、CDN、Service Worker等技术,可以显著提升用户体验并降低服务器成本。
核心原则:
- 静态资源长期缓存:使用文件名哈希+max-age=31536000
- HTML文档谨慎缓存:使用no-cache或短max-age
- API响应按需缓存:根据数据时效性设置合适的max-age
- 监控与调优:持续监控缓存命中率并优化策略
通过本文介绍的策略和实践,您可以构建一个高性能、高可靠性的缓存系统,为用户提供流畅的Web体验。
