引言:HTTP缓存的重要性
HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和中间代理服务器上存储资源副本,显著减少网络请求、降低服务器负载并提升用户体验。根据Google的研究,将页面加载时间从1秒增加到3秒,跳出率会增加32%;而HTTP缓存策略的合理配置,通常能将重复访问的页面加载时间减少50%以上。
HTTP缓存机制主要依赖于HTTP协议中的头部字段(Headers)来控制资源的缓存行为。理解这些头部字段的工作原理,对于构建高性能的Web应用至关重要。本文将深入解析HTTP缓存的各个层面,从基础概念到高级策略,并提供实际的代码示例和问题排查指南。
HTTP缓存基础:缓存的分类与工作流程
在深入细节之前,我们需要理解HTTP缓存的两个主要分类:
1. 强缓存(Strong Caching)
强缓存是最快的缓存机制,它直接从浏览器本地缓存读取资源,不会向服务器发送任何请求。强缓存主要通过以下两个响应头控制:
Cache-ControlExpires
2. 协商缓存(Negotiated Caching)
当强缓存过期或不适用时,浏览器会向服务器发起请求,进行缓存有效性验证。如果资源未修改,服务器返回304状态码(Not Modified),浏览器继续使用本地缓存。协商缓存主要通过以下头部控制:
Last-Modified/If-Modified-SinceETag/If-None-Match
HTTP缓存工作流程图解
浏览器发起请求
↓
检查强缓存(Cache-Control/Expires)
↓
┌─── 未过期? → 直接使用本地缓存(200 from memory cache)
│
└─── 已过期? → 向服务器发起请求
↓
携带协商缓存头部(If-None-Match/If-Modified-Since)
↓
服务器验证资源状态
↓
┌─── 资源未修改? → 返回304 Not Modified
│
└─── 资源已修改? → 返回200 OK + 新资源
↓
浏览器更新缓存
强缓存详解:Cache-Control与Expires
Cache-Control:现代缓存控制的核心
Cache-Control是HTTP/1.1规范中引入的头部,用于精确控制缓存行为,它比Expires更灵活且优先级更高。它可以在请求头和响应头中使用,但在缓存策略中主要用在响应头中。
常用指令详解
publicvsprivatepublic:响应可以被任何缓存(包括CDN、代理服务器)缓存private:响应只能被用户浏览器缓存,不能被中间代理缓存
示例场景:
# 用户个人数据页面,只能在浏览器缓存
Cache-Control: private, max-age=3600
# 静态资源,可以被CDN缓存
Cache-Control: public, max-age=31536000
max-age与s-maxagemax-age:指定资源在浏览器中的最大缓存时间(秒)s-maxage:指定资源在共享缓存(如CDN)中的最大缓存时间,优先级高于max-age
代码示例:
# 浏览器缓存1小时,CDN缓存24小时
Cache-Control: public, max-age=3600, s-maxage=86400
no-cache与no-storeno-cache:不是不缓存,而是每次使用缓存前必须向服务器验证(相当于强制协商缓存)no-store:完全不缓存,每次都要从服务器重新获取
常见误解澄清:
# 错误理解:no-cache = 不缓存
# 正确理解:no-cache = 可以缓存,但使用前必须验证
# 典型应用场景
Cache-Control: no-cache # 用于需要实时验证的资源,如API响应
Cache-Control: no-store # 用于敏感数据,如银行交易页面
must-revalidate与proxy-revalidatemust-revalidate:缓存过期后必须向服务器验证,不能使用过期缓存proxy-revalidate:仅对共享缓存(如CDN)有效
示例:
# 关键资源,过期后必须重新验证
Cache-Control: public, max-age=600, must-revalidate
Expires:传统的过期时间控制
Expires是HTTP/1.0时代的产物,它指定一个绝对过期时间。由于它依赖于客户端和服务器的时钟同步,且精度只能到秒,现代应用中已逐渐被Cache-Control取代。
示例:
Expires: Wed, 21 Oct 2025 07:28:00 GMT
优先级规则:
- 如果同时存在
Cache-Control: max-age和Expires,max-age会覆盖Expires - 但为了兼容旧客户端,建议同时设置两者
协商缓存详解:ETag与Last-Modified
当强缓存过期后,浏览器会发起带有协商缓存头部的请求,服务器根据这些头部判断资源是否需要重新传输。
Last-Modified与If-Modified-Since
工作原理:
- 服务器首次响应时,返回
Last-Modified头部,表示资源的最后修改时间 - 浏览器下次请求时,携带
If-Modified-Since头部,值为上次收到的Last-Modified - 服务器比较资源的实际修改时间和
If-Modified-Since:- 如果资源未修改,返回304状态码
- 如果资源已修改,返回200状态码和新资源
代码示例:
# 首次响应
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
Cache-Control: max-age=86400
# 浏览器下次请求(强缓存过期后)
GET /style.css HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
# 服务器响应(资源未修改)
HTTP/1.1 304 Not Modified
局限性:
- 时间精度只能到秒
- 如果文件内容改变但修改时间未变(如某些编辑器操作),会导致缓存失效
- 无法检测文件重命名等情况
ETag与If-None-Match
ETag(Entity Tag)是资源的唯一标识符,通常基于文件内容的哈希值生成,解决了Last-Modified的局限性。
工作原理:
- 服务器首次响应时,返回
ETag头部,表示资源的唯一标识 - 浏览器下次请求时,携带
If-None-Match头部,值为上次收到的ETag - 服务器比较资源的实际ETag和
If-None-Match:- 如果匹配,返回304状态码
- 如果不匹配,返回200状态码和新资源
代码示例:
# 首次响应
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=86400
# 浏览器下次请求
GET /style.css HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 服务器响应(资源未修改)
HTTP/1.1 304 Not Modified
ETag的类型:
- 强ETag:资源内容的任何变化都会改变ETag值
- 弱ETag:只在资源内容发生重大变化时改变ETag值,格式为
W/"版本号"
示例:
ETag: "版本号" # 强ETag
ETag: W/"版本号" # 弱ETag
缓存策略配置:不同资源的最佳实践
1. HTML文件:谨慎缓存
HTML文件通常包含动态内容和资源引用,应避免过度缓存。
推荐配置:
Cache-Control: no-cache 或 max-age=0, must-revalidate
原因:
- 确保用户总能获取最新的HTML,从而加载最新的资源引用
- 避免用户因缓存旧HTML而加载过时的JS/CSS
2. 静态资源(JS/CSS/图片):长期缓存
静态资源应使用文件名哈希或版本号,实现长期缓存。
推荐配置:
Cache-Control: public, max-age=31536000, immutable
代码示例:
// Webpack配置示例:生成带哈希的文件名
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
}
}
部署流程:
- 构建时生成带哈希的文件名:
app.a1b2c3d4.js - HTML文件引用新文件名:
<script src="app.a1b2c3d4.js"></script> - 设置长期缓存:
Cache-Control: public, max-age=31536000, immutable - 当代码更新时,哈希值改变,浏览器自动加载新文件
3. API响应:动态控制
API响应的缓存策略应根据业务需求动态调整。
推荐配置:
# 实时数据,不缓存
Cache-Control: no-store
# 可缓存1分钟的数据
Cache-Control: private, max-age=60
# 需要实时验证的数据
Cache-Control: no-cache
4. 字体文件:长期缓存
字体文件通常不经常变化,适合长期缓存。
推荐配置:
Cache-Control: public, max-age=31536000, immutable
服务器端实现示例
Node.js (Express) 实现
const express = require('express');
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const app = express();
// 计算文件ETag的辅助函数
function calculateETag(filePath) {
const fileBuffer = fs.readFileSync(filePath);
return crypto.createHash('md5').update(fileBuffer).digest('hex');
}
// 静态资源中间件(带缓存控制)
app.use('/static', (req, res, next) => {
const filePath = path.join(__dirname, 'public', req.path);
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
return next();
}
const stats = fs.statSync(filePath);
const etag = calculateETag(filePath);
const lastModified = stats.mtime.toUTCString();
// 设置协商缓存头部
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified);
// 检查协商缓存
const ifNoneMatch = req.headers['if-none-match'];
const ifModifiedSince = req.headers['if-modified-since'];
if ((ifNoneMatch && ifNoneMatch === etag) ||
(ifModifiedSince && ifModifiedSince === lastModified)) {
res.status(304).end();
return;
}
// 设置强缓存
const ext = path.extname(filePath);
if (['.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2'].includes(ext)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else {
res.setHeader('Cache-Control', 'no-cache');
}
// 发送文件
res.sendFile(filePath);
});
// API接口示例
app.get('/api/user/:id', (req, res) => {
// 根据业务需求动态设置缓存
const userId = req.params.id;
if (req.query.refresh === 'true') {
// 强制刷新,不缓存
res.setHeader('Cache-Control', 'no-store');
} else {
// 可缓存30秒
res.setHeader('Cache-Control', 'private, max-age=30');
}
// 设置ETag
const userData = { id: userId, name: 'John Doe', timestamp: Date.now() };
const etag = crypto.createHash('md5').update(JSON.stringify(userData)).digest('hex');
res.setHeader('ETag', etag);
// 检查协商缓存
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(userData);
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Nginx 配置示例
# 静态资源缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
root /path/to/your/static/files;
# 强缓存1年
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
# 协商缓存
etag on;
add_header Last-Modified $date_gmt;
# 开启Gzip压缩
gzip on;
gzip_types text/css application/javascript image/svg+xml;
}
# HTML文件配置
location ~* \.html$ {
root /path/to/your/html/files;
# 不缓存HTML
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
# 仍然开启ETag用于验证
etag on;
}
# API接口配置
location /api/ {
proxy_pass http://backend_server;
# 默认不缓存
add_header Cache-Control "no-store";
# 根据请求参数动态调整(需要Lua模块或后端设置)
# 这里仅作为示例,实际应在后端代码中处理
}
Apache .htaccess 配置
# 启用mod_headers和mod_expires
<IfModule mod_expires.c>
ExpiresActive On
# HTML文件 - 不缓存
ExpiresByType text/html "access plus 0 seconds"
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
# CSS和JavaScript - 1年
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
Header set Cache-Control "public, max-age=31536000, immutable"
# 图片 - 1年
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
Header set Cache-Control "public, max-age=31536000, immutable"
# 字体文件 - 1年
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
Header set Cache-Control "public, max-age=31536000, immutable"
</IfModule>
# ETag配置
<IfModule mod_headers.c>
# 移除ETag(可选,如果使用文件哈希命名)
# Header unset ETag
</IfModule>
# Gzip压缩
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript
</IfModule>
常见缓存失效问题及解决方案
问题1:缓存策略未生效
症状:浏览器仍然缓存了旧版本的资源,导致更新后用户看不到变化。
排查步骤:
- 检查浏览器开发者工具的Network面板,确认响应头是否包含正确的
Cache-Control和ETag - 检查资源URL是否包含查询参数(如
?v=1),查询参数可能会影响缓存行为 - 确认服务器配置是否正确应用
解决方案:
// 使用文件哈希代替查询参数
// 错误做法:app.js?v=123
// 正确做法:app.123abc.js
// Webpack配置
output: {
filename: '[name].[contenthash:8].js'
}
问题2:304状态码未返回
症状:即使资源未修改,服务器仍然返回200状态码和完整资源。
可能原因:
- 服务器未正确设置
ETag或Last-Modified - 客户端请求未携带正确的
If-None-Match或If-Modified-Since - 服务器端逻辑错误,未正确处理协商缓存头部
排查代码:
// Express中间件调试版
app.use((req, res, next) => {
console.log('Request headers:', req.headers);
console.log('If-None-Match:', req.headers['if-none-match']);
console.log('If-Modified-Since:', req.headers['if-modified-since']);
next();
});
问题3:CDN缓存导致更新延迟
症状:更新资源后,部分用户仍然看到旧版本,特别是通过CDN访问时。
原因:CDN节点缓存了旧资源,且缓存时间较长。
解决方案:
- 文件名哈希:最可靠的方案,确保文件内容变化时URL必然变化
- CDN缓存刷新:手动刷新CDN缓存(不同CDN提供商API不同)
- 设置合适的s-maxage:控制CDN缓存时间
AWS CloudFront刷新示例:
const AWS = require('aws-sdk');
const cloudfront = new AWS.CloudFront();
// 创建刷新请求
const params = {
DistributionId: 'YOUR_DISTRIBUTION_ID',
InvalidationBatch: {
Paths: {
Quantity: 2,
Items: [
'/static/app.*.js',
'/static/style.*.css'
]
},
CallerReference: Date.now().toString()
}
};
cloudfront.createInvalidation(params, (err, data) => {
if (err) console.error(err);
else console.log('Cache invalidation created:', data);
});
问题4:浏览器缓存空间不足
症状:在某些设备上,缓存被意外清除或无法存储。
原因:浏览器缓存空间有限,可能被其他资源挤占。
解决方案:
- 使用Service Worker实现更精细的缓存控制
- 合理设置缓存时间,避免缓存过多不必要资源
Service Worker示例:
// sw.js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/static/app.a1b2c3d4.js',
'/static/style.e5f6g7h8.css'
];
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 => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
问题5:移动端缓存异常
症状:iOS Safari或Android WebView中缓存行为与桌面浏览器不一致。
原因:移动端浏览器对缓存的实现可能存在差异,特别是WebView组件。
解决方案:
- 避免使用
no-cache:在某些旧版本WebView中可能不被正确解析 - 使用
must-revalidate:确保过期缓存被正确验证 - 测试具体设备:在目标设备上实际测试缓存行为
移动端优化配置:
# 兼容性更好的配置
Cache-Control: public, max-age=3600, must-revalidate
高级缓存策略
1. 缓存键(Cache Key)优化
在多租户或个性化内容场景下,需要根据请求特征生成不同的缓存键。
Nginx Lua示例:
# 需要安装ngx_http_lua_module
location /api/personalized {
access_by_lua_block {
local user_id = ngx.var.http_x_user_id or "anonymous"
local accept_language = ngx.var.http_accept_language or "default"
-- 生成自定义缓存键
ngx.var.cache_key = user_id .. "|" .. accept_language .. "|" .. ngx.var.uri
}
proxy_pass http://backend;
proxy_cache_valid 200 5m;
}
2. 缓存分层策略
// 多层缓存示例
const cacheLayers = {
// L1: 内存缓存(最快)
memory: new Map(),
// L2: Redis缓存(分布式)
redis: require('redis').createClient(),
// L3: CDN缓存(边缘节点)
cdn: {
// 通过设置Cache-Control头部控制
setHeaders: (res, maxAge) => {
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
}
}
};
// 获取数据的多层缓存逻辑
async function getDataWithCache(key, fetchFn, options = {}) {
const { memoryTtl = 60, redisTtl = 300, cdnTtl = 3600 } = options;
// L1: 检查内存缓存
if (cacheLayers.memory.has(key)) {
return cacheLayers.memory.get(key);
}
// L2: 检查Redis缓存
try {
const redisData = await cacheLayers.redis.get(key);
if (redisData) {
const parsed = JSON.parse(redisData);
// 回填内存缓存
cacheLayers.memory.set(key, parsed, memoryTtl * 1000);
return parsed;
}
} catch (err) {
console.error('Redis error:', err);
}
// L3: 回源获取
const data = await fetchFn();
// 回填所有缓存层
cacheLayers.memory.set(key, data, memoryTtl * 1000);
try {
await cacheLayers.redis.setex(key, redisTtl, JSON.stringify(data));
} catch (err) {
console.error('Redis set error:', err);
}
// 返回数据,由HTTP层设置CDN缓存头部
return data;
}
3. 缓存预热
在部署新版本后,主动预热缓存,避免用户首次访问时遇到性能问题。
// 部署后预热脚本
const axios = require('axios');
const criticalUrls = [
'/',
'/static/app.a1b2c3d4.js',
'/static/style.e5f6g7h8.css',
'/api/config'
];
async function warmupCache() {
console.log('开始预热缓存...');
for (const url of criticalUrls) {
try {
const start = Date.now();
const response = await axios.get(`http://localhost:3000${url}`);
const duration = Date.now() - start;
console.log(`✓ ${url} - ${response.status} - ${duration}ms`);
console.log(` Cache-Control: ${response.headers['cache-control']}`);
console.log(` ETag: ${response.headers['etag']}`);
} catch (error) {
console.error(`✗ ${url} - ${error.message}`);
}
}
console.log('预热完成!');
}
warmupCache();
缓存监控与性能分析
1. 使用Chrome DevTools分析缓存
步骤:
- 打开DevTools → Network面板
- 勾选”Disable cache”来测试无缓存情况
- 取消勾选后刷新页面,观察哪些资源返回304
- 查看Size列:
(memory cache)或(disk cache)表示强缓存命中304表示协商缓存命中200表示无缓存或缓存失效
2. 使用Lighthouse进行缓存审计
Lighthouse可以检测缓存策略是否合理:
# 运行Lighthouse CLI
lighthouse https://your-site.com --output=json --output-path=report.json
# 检查"Serve static assets with an efficient cache policy"审计项
3. 服务器端缓存命中率监控
// Express中间件:记录缓存命中率
const cacheStats = {
hits: 0,
misses: 0,
total: 0
};
app.use((req, res, next) => {
const originalEnd = res.end;
res.end = function(...args) {
cacheStats.total++;
if (res.statusCode === 304) {
cacheStats.hits++;
} else if (res.statusCode === 200) {
cacheStats.misses++;
}
// 每100个请求打印一次统计
if (cacheStats.total % 100 === 0) {
const hitRate = (cacheStats.hits / cacheStats.total * 100).toFixed(2);
console.log(`Cache Hit Rate: ${hitRate}% (${cacheStats.hits}/${cacheStats.total})`);
}
originalEnd.apply(this, args);
};
next();
});
总结与最佳实践清单
✅ 最佳实践清单
- HTML文件:使用
Cache-Control: no-cache或max-age=0, must-revalidate - 静态资源:使用文件名哈希 +
Cache-Control: public, max-age=31536000, immutable - API响应:根据业务需求动态设置,通常使用
no-store或短时间max-age - 始终设置ETag:启用协商缓存,减少不必要的数据传输
- 避免使用查询参数缓存:使用文件名哈希代替
- CDN配置:设置合适的
s-maxage,考虑使用文件名哈希避免CDN刷新 - 移动端测试:在目标设备上实际测试缓存行为
- 监控缓存命中率:定期检查并优化缓存策略
❌ 常见错误
- HTML文件长期缓存:导致用户无法获取最新版本
- 使用查询参数缓存:
app.js?v=1可能被代理服务器忽略 - 忽略ETag:浪费协商缓存机会
- 所有资源使用相同缓存策略:未区分动态和静态内容
- 不测试移动端:WebView缓存行为可能不同
通过合理配置HTTP缓存策略,你可以显著提升网站性能,减少服务器负载,并为用户提供更快的加载体验。记住,缓存策略不是一成不变的,需要根据业务需求和用户反馈持续优化。# 深入解析HTTP缓存策略与实现:如何有效提升网站性能并解决常见缓存失效问题
引言:HTTP缓存的重要性
HTTP缓存是Web性能优化的核心技术之一,它通过在客户端(浏览器)和中间代理服务器上存储资源副本,显著减少网络请求、降低服务器负载并提升用户体验。根据Google的研究,将页面加载时间从1秒增加到3秒,跳出率会增加32%;而HTTP缓存策略的合理配置,通常能将重复访问的页面加载时间减少50%以上。
HTTP缓存机制主要依赖于HTTP协议中的头部字段(Headers)来控制资源的缓存行为。理解这些头部字段的工作原理,对于构建高性能的Web应用至关重要。本文将深入解析HTTP缓存的各个层面,从基础概念到高级策略,并提供实际的代码示例和问题排查指南。
HTTP缓存基础:缓存的分类与工作流程
在深入细节之前,我们需要理解HTTP缓存的两个主要分类:
1. 强缓存(Strong Caching)
强缓存是最快的缓存机制,它直接从浏览器本地缓存读取资源,不会向服务器发送任何请求。强缓存主要通过以下两个响应头控制:
Cache-ControlExpires
2. 协商缓存(Negotiated Caching)
当强缓存过期或不适用时,浏览器会向服务器发起请求,进行缓存有效性验证。如果资源未修改,服务器返回304状态码(Not Modified),浏览器继续使用本地缓存。协商缓存主要通过以下头部控制:
Last-Modified/If-Modified-SinceETag/If-None-Match
HTTP缓存工作流程图解
浏览器发起请求
↓
检查强缓存(Cache-Control/Expires)
↓
┌─── 未过期? → 直接使用本地缓存(200 from memory cache)
│
└─── 已过期? → 向服务器发起请求
↓
携带协商缓存头部(If-None-Match/If-Modified-Since)
↓
服务器验证资源状态
↓
┌─── 资源未修改? → 返回304 Not Modified
│
└─── 资源已修改? → 返回200 OK + 新资源
↓
浏览器更新缓存
强缓存详解:Cache-Control与Expires
Cache-Control:现代缓存控制的核心
Cache-Control是HTTP/1.1规范中引入的头部,用于精确控制缓存行为,它比Expires更灵活且优先级更高。它可以在请求头和响应头中使用,但在缓存策略中主要用在响应头中。
常用指令详解
publicvsprivatepublic:响应可以被任何缓存(包括CDN、代理服务器)缓存private:响应只能被用户浏览器缓存,不能被中间代理缓存
示例场景:
# 用户个人数据页面,只能在浏览器缓存
Cache-Control: private, max-age=3600
# 静态资源,可以被CDN缓存
Cache-Control: public, max-age=31536000
max-age与s-maxagemax-age:指定资源在浏览器中的最大缓存时间(秒)s-maxage:指定资源在共享缓存(如CDN)中的最大缓存时间,优先级高于max-age
代码示例:
# 浏览器缓存1小时,CDN缓存24小时
Cache-Control: public, max-age=3600, s-maxage=86400
no-cache与no-storeno-cache:不是不缓存,而是每次使用缓存前必须向服务器验证(相当于强制协商缓存)no-store:完全不缓存,每次都要从服务器重新获取
常见误解澄清:
# 错误理解:no-cache = 不缓存
# 正确理解:no-cache = 可以缓存,但使用前必须验证
# 典型应用场景
Cache-Control: no-cache # 用于需要实时验证的资源,如API响应
Cache-Control: no-store # 用于敏感数据,如银行交易页面
must-revalidate与proxy-revalidatemust-revalidate:缓存过期后必须向服务器验证,不能使用过期缓存proxy-revalidate:仅对共享缓存(如CDN)有效
示例:
# 关键资源,过期后必须重新验证
Cache-Control: public, max-age=600, must-revalidate
Expires:传统的过期时间控制
Expires是HTTP/1.0时代的产物,它指定一个绝对过期时间。由于它依赖于客户端和服务器的时钟同步,且精度只能到秒,现代应用中已逐渐被Cache-Control取代。
示例:
Expires: Wed, 21 Oct 2025 07:28:00 GMT
优先级规则:
- 如果同时存在
Cache-Control: max-age和Expires,max-age会覆盖Expires - 但为了兼容旧客户端,建议同时设置两者
协商缓存详解:ETag与Last-Modified
当强缓存过期后,浏览器会发起带有协商缓存头部的请求,服务器根据这些头部判断资源是否需要重新传输。
Last-Modified与If-Modified-Since
工作原理:
- 服务器首次响应时,返回
Last-Modified头部,表示资源的最后修改时间 - 浏览器下次请求时,携带
If-Modified-Since头部,值为上次收到的Last-Modified - 服务器比较资源的实际修改时间和
If-Modified-Since:- 如果资源未修改,返回304状态码
- 如果资源已修改,返回200状态码和新资源
代码示例:
# 首次响应
HTTP/1.1 200 OK
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
Cache-Control: max-age=86400
# 浏览器下次请求(强缓存过期后)
GET /style.css HTTP/1.1
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
# 服务器响应(资源未修改)
HTTP/1.1 304 Not Modified
局限性:
- 时间精度只能到秒
- 如果文件内容改变但修改时间未变(如某些编辑器操作),会导致缓存失效
- 无法检测文件重命名等情况
ETag与If-None-Match
ETag(Entity Tag)是资源的唯一标识符,通常基于文件内容的哈希值生成,解决了Last-Modified的局限性。
工作原理:
- 服务器首次响应时,返回
ETag头部,表示资源的唯一标识 - 浏览器下次请求时,携带
If-None-Match头部,值为上次收到的ETag - 服务器比较资源的实际ETag和
If-None-Match:- 如果匹配,返回304状态码
- 如果不匹配,返回200状态码和新资源
代码示例:
# 首次响应
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: max-age=86400
# 浏览器下次请求
GET /style.css HTTP/1.1
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
# 服务器响应(资源未修改)
HTTP/1.1 304 Not Modified
ETag的类型:
- 强ETag:资源内容的任何变化都会改变ETag值
- 弱ETag:只在资源内容发生重大变化时改变ETag值,格式为
W/"版本号"
示例:
ETag: "版本号" # 强ETag
ETag: W/"版本号" # 弱ETag
缓存策略配置:不同资源的最佳实践
1. HTML文件:谨慎缓存
HTML文件通常包含动态内容和资源引用,应避免过度缓存。
推荐配置:
Cache-Control: no-cache 或 max-age=0, must-revalidate
原因:
- 确保用户总能获取最新的HTML,从而加载最新的资源引用
- 避免用户因缓存旧HTML而加载过时的JS/CSS
2. 静态资源(JS/CSS/图片):长期缓存
静态资源应使用文件名哈希或版本号,实现长期缓存。
推荐配置:
Cache-Control: public, max-age=31536000, immutable
代码示例:
// Webpack配置示例:生成带哈希的文件名
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
}
}
部署流程:
- 构建时生成带哈希的文件名:
app.a1b2c3d4.js - HTML文件引用新文件名:
<script src="app.a1b2c3d4.js"></script> - 设置长期缓存:
Cache-Control: public, max-age=31536000, immutable - 当代码更新时,哈希值改变,浏览器自动加载新文件
3. API响应:动态控制
API响应的缓存策略应根据业务需求动态调整。
推荐配置:
# 实时数据,不缓存
Cache-Control: no-store
# 可缓存1分钟的数据
Cache-Control: private, max-age=60
# 需要实时验证的数据
Cache-Control: no-cache
4. 字体文件:长期缓存
字体文件通常不经常变化,适合长期缓存。
推荐配置:
Cache-Control: public, max-age=31536000, immutable
服务器端实现示例
Node.js (Express) 实现
const express = require('express');
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const app = express();
// 计算文件ETag的辅助函数
function calculateETag(filePath) {
const fileBuffer = fs.readFileSync(filePath);
return crypto.createHash('md5').update(fileBuffer).digest('hex');
}
// 静态资源中间件(带缓存控制)
app.use('/static', (req, res, next) => {
const filePath = path.join(__dirname, 'public', req.path);
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
return next();
}
const stats = fs.statSync(filePath);
const etag = calculateETag(filePath);
const lastModified = stats.mtime.toUTCString();
// 设置协商缓存头部
res.setHeader('ETag', etag);
res.setHeader('Last-Modified', lastModified);
// 检查协商缓存
const ifNoneMatch = req.headers['if-none-match'];
const ifModifiedSince = req.headers['if-modified-since'];
if ((ifNoneMatch && ifNoneMatch === etag) ||
(ifModifiedSince && ifModifiedSince === lastModified)) {
res.status(304).end();
return;
}
// 设置强缓存
const ext = path.extname(filePath);
if (['.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2'].includes(ext)) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
} else {
res.setHeader('Cache-Control', 'no-cache');
}
// 发送文件
res.sendFile(filePath);
});
// API接口示例
app.get('/api/user/:id', (req, res) => {
// 根据业务需求动态设置缓存
const userId = req.params.id;
if (req.query.refresh === 'true') {
// 强制刷新,不缓存
res.setHeader('Cache-Control', 'no-store');
} else {
// 可缓存30秒
res.setHeader('Cache-Control', 'private, max-age=30');
}
// 设置ETag
const userData = { id: userId, name: 'John Doe', timestamp: Date.now() };
const etag = crypto.createHash('md5').update(JSON.stringify(userData)).digest('hex');
res.setHeader('ETag', etag);
// 检查协商缓存
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.json(userData);
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Nginx 配置示例
# 静态资源缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
root /path/to/your/static/files;
# 强缓存1年
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable";
# 协商缓存
etag on;
add_header Last-Modified $date_gmt;
# 开启Gzip压缩
gzip on;
gzip_types text/css application/javascript image/svg+xml;
}
# HTML文件配置
location ~* \.html$ {
root /path/to/your/html/files;
# 不缓存HTML
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
# 仍然开启ETag用于验证
etag on;
}
# API接口配置
location /api/ {
proxy_pass http://backend_server;
# 默认不缓存
add_header Cache-Control "no-store";
# 根据请求参数动态调整(需要Lua模块或后端设置)
# 这里仅作为示例,实际应在后端代码中处理
}
Apache .htaccess 配置
# 启用mod_headers和mod_expires
<IfModule mod_expires.c>
ExpiresActive On
# HTML文件 - 不缓存
ExpiresByType text/html "access plus 0 seconds"
Header set Cache-Control "no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
# CSS和JavaScript - 1年
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
Header set Cache-Control "public, max-age=31536000, immutable"
# 图片 - 1年
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
Header set Cache-Control "public, max-age=31536000, immutable"
# 字体文件 - 1年
ExpiresByType font/woff "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
Header set Cache-Control "public, max-age=31536000, immutable"
</IfModule>
# ETag配置
<IfModule mod_headers.c>
# 移除ETag(可选,如果使用文件哈希命名)
# Header unset ETag
</IfModule>
# Gzip压缩
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript
</IfModule>
常见缓存失效问题及解决方案
问题1:缓存策略未生效
症状:浏览器仍然缓存了旧版本的资源,导致更新后用户看不到变化。
排查步骤:
- 检查浏览器开发者工具的Network面板,确认响应头是否包含正确的
Cache-Control和ETag - 检查资源URL是否包含查询参数(如
?v=1),查询参数可能会影响缓存行为 - 确认服务器配置是否正确应用
解决方案:
// 使用文件哈希代替查询参数
// 错误做法:app.js?v=123
// 正确做法:app.123abc.js
// Webpack配置
output: {
filename: '[name].[contenthash:8].js'
}
问题2:304状态码未返回
症状:即使资源未修改,服务器仍然返回200状态码和完整资源。
可能原因:
- 服务器未正确设置
ETag或Last-Modified - 客户端请求未携带正确的
If-None-Match或If-Modified-Since - 服务器端逻辑错误,未正确处理协商缓存头部
排查代码:
// Express中间件调试版
app.use((req, res, next) => {
console.log('Request headers:', req.headers);
console.log('If-None-Match:', req.headers['if-none-match']);
console.log('If-Modified-Since:', req.headers['if-modified-since']);
next();
});
问题3:CDN缓存导致更新延迟
症状:更新资源后,部分用户仍然看到旧版本,特别是通过CDN访问时。
原因:CDN节点缓存了旧资源,且缓存时间较长。
解决方案:
- 文件名哈希:最可靠的方案,确保文件内容变化时URL必然变化
- CDN缓存刷新:手动刷新CDN缓存(不同CDN提供商API不同)
- 设置合适的s-maxage:控制CDN缓存时间
AWS CloudFront刷新示例:
const AWS = require('aws-sdk');
const cloudfront = new AWS.CloudFront();
// 创建刷新请求
const params = {
DistributionId: 'YOUR_DISTRIBUTION_ID',
InvalidationBatch: {
Paths: {
Quantity: 2,
Items: [
'/static/app.*.js',
'/static/style.*.css'
]
},
CallerReference: Date.now().toString()
}
};
cloudfront.createInvalidation(params, (err, data) => {
if (err) console.error(err);
else console.log('Cache invalidation created:', data);
});
问题4:浏览器缓存空间不足
症状:在某些设备上,缓存被意外清除或无法存储。
原因:浏览器缓存空间有限,可能被其他资源挤占。
解决方案:
- 使用Service Worker实现更精细的缓存控制
- 合理设置缓存时间,避免缓存过多不必要资源
Service Worker示例:
// sw.js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/static/app.a1b2c3d4.js',
'/static/style.e5f6g7h8.css'
];
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 => {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
问题5:移动端缓存异常
症状:iOS Safari或Android WebView中缓存行为与桌面浏览器不一致。
原因:移动端浏览器对缓存的实现可能存在差异,特别是WebView组件。
解决方案:
- 避免使用
no-cache:在某些旧版本WebView中可能不被正确解析 - 使用
must-revalidate:确保过期缓存被正确验证 - 测试具体设备:在目标设备上实际测试缓存行为
移动端优化配置:
# 兼容性更好的配置
Cache-Control: public, max-age=3600, must-revalidate
高级缓存策略
1. 缓存键(Cache Key)优化
在多租户或个性化内容场景下,需要根据请求特征生成不同的缓存键。
Nginx Lua示例:
# 需要安装ngx_http_lua_module
location /api/personalized {
access_by_lua_block {
local user_id = ngx.var.http_x_user_id or "anonymous"
local accept_language = ngx.var.http_accept_language or "default"
-- 生成自定义缓存键
ngx.var.cache_key = user_id .. "|" .. accept_language .. "|" .. ngx.var.uri
}
proxy_pass http://backend;
proxy_cache_valid 200 5m;
}
2. 缓存分层策略
// 多层缓存示例
const cacheLayers = {
// L1: 内存缓存(最快)
memory: new Map(),
// L2: Redis缓存(分布式)
redis: require('redis').createClient(),
// L3: CDN缓存(边缘节点)
cdn: {
// 通过设置Cache-Control头部控制
setHeaders: (res, maxAge) => {
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
}
}
};
// 获取数据的多层缓存逻辑
async function getDataWithCache(key, fetchFn, options = {}) {
const { memoryTtl = 60, redisTtl = 300, cdnTtl = 3600 } = options;
// L1: 检查内存缓存
if (cacheLayers.memory.has(key)) {
return cacheLayers.memory.get(key);
}
// L2: 检查Redis缓存
try {
const redisData = await cacheLayers.redis.get(key);
if (redisData) {
const parsed = JSON.parse(redisData);
// 回填内存缓存
cacheLayers.memory.set(key, parsed, memoryTtl * 1000);
return parsed;
}
} catch (err) {
console.error('Redis error:', err);
}
// L3: 回源获取
const data = await fetchFn();
// 回填所有缓存层
cacheLayers.memory.set(key, data, memoryTtl * 1000);
try {
await cacheLayers.redis.setex(key, redisTtl, JSON.stringify(data));
} catch (err) {
console.error('Redis set error:', err);
}
// 返回数据,由HTTP层设置CDN缓存头部
return data;
}
3. 缓存预热
在部署新版本后,主动预热缓存,避免用户首次访问时遇到性能问题。
// 部署后预热脚本
const axios = require('axios');
const criticalUrls = [
'/',
'/static/app.a1b2c3d4.js',
'/static/style.e5f6g7h8.css',
'/api/config'
];
async function warmupCache() {
console.log('开始预热缓存...');
for (const url of criticalUrls) {
try {
const start = Date.now();
const response = await axios.get(`http://localhost:3000${url}`);
const duration = Date.now() - start;
console.log(`✓ ${url} - ${response.status} - ${duration}ms`);
console.log(` Cache-Control: ${response.headers['cache-control']}`);
console.log(` ETag: ${response.headers['etag']}`);
} catch (error) {
console.error(`✗ ${url} - ${error.message}`);
}
}
console.log('预热完成!');
}
warmupCache();
缓存监控与性能分析
1. 使用Chrome DevTools分析缓存
步骤:
- 打开DevTools → Network面板
- 勾选”Disable cache”来测试无缓存情况
- 取消勾选后刷新页面,观察哪些资源返回304
- 查看Size列:
(memory cache)或(disk cache)表示强缓存命中304表示协商缓存命中200表示无缓存或缓存失效
2. 使用Lighthouse进行缓存审计
Lighthouse可以检测缓存策略是否合理:
# 运行Lighthouse CLI
lighthouse https://your-site.com --output=json --output-path=report.json
# 检查"Serve static assets with an efficient cache policy"审计项
3. 服务器端缓存命中率监控
// Express中间件:记录缓存命中率
const cacheStats = {
hits: 0,
misses: 0,
total: 0
};
app.use((req, res, next) => {
const originalEnd = res.end;
res.end = function(...args) {
cacheStats.total++;
if (res.statusCode === 304) {
cacheStats.hits++;
} else if (res.statusCode === 200) {
cacheStats.misses++;
}
// 每100个请求打印一次统计
if (cacheStats.total % 100 === 0) {
const hitRate = (cacheStats.hits / cacheStats.total * 100).toFixed(2);
console.log(`Cache Hit Rate: ${hitRate}% (${cacheStats.hits}/${cacheStats.total})`);
}
originalEnd.apply(this, args);
};
next();
});
总结与最佳实践清单
✅ 最佳实践清单
- HTML文件:使用
Cache-Control: no-cache或max-age=0, must-revalidate - 静态资源:使用文件名哈希 +
Cache-Control: public, max-age=31536000, immutable - API响应:根据业务需求动态设置,通常使用
no-store或短时间max-age - 始终设置ETag:启用协商缓存,减少不必要的数据传输
- 避免使用查询参数缓存:使用文件名哈希代替
- CDN配置:设置合适的
s-maxage,考虑使用文件名哈希避免CDN刷新 - 移动端测试:在目标设备上实际测试缓存行为
- 监控缓存命中率:定期检查并优化缓存策略
❌ 常见错误
- HTML文件长期缓存:导致用户无法获取最新版本
- 使用查询参数缓存:
app.js?v=1可能被代理服务器忽略 - 忽略ETag:浪费协商缓存机会
- 所有资源使用相同缓存策略:未区分动态和静态内容
- 不测试移动端:WebView缓存行为可能不同
通过合理配置HTTP缓存策略,你可以显著提升网站性能,减少服务器负载,并为用户提供更快的加载体验。记住,缓存策略不是一成不变的,需要根据业务需求和用户反馈持续优化。
