缓存策略
缓存概述
缓存是提升性能的重要手段,主要包括:
- 浏览器缓存:HTTP 缓存
- Service Worker 缓存:离线缓存
- 应用缓存:内存缓存、本地存储
HTTP 缓存
1. 强缓存
Cache-Control:
Cache-Control: max-age=3600
Cache-Control: public, max-age=3600
Cache-Control: private, max-age=3600
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: must-revalidate
常用指令:
max-age:缓存有效期(秒)public:可以被任何缓存存储private:只能被浏览器缓存no-cache:需要验证缓存no-store:不缓存must-revalidate:过期后必须验证
Expires:
Expires: Wed, 21 Oct 2025 07:28:00 GMT
优先级: Cache-Control > Expires
2. 协商缓存
ETag:
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified:
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
优先级: ETag > Last-Modified
3. 缓存策略
静态资源(长期缓存):
Cache-Control: public, max-age=31536000, immutable
HTML(不缓存或短缓存):
Cache-Control: no-cache
# 或
Cache-Control: max-age=0, must-revalidate
API 响应(短缓存):
Cache-Control: private, max-age=300
Service Worker 缓存
1. 缓存策略
Cache First(缓存优先):
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(response => {
const responseClone = response.clone();
caches.open('v1').then(cache => {
cache.put(event.request, responseClone);
});
return response;
});
})
);
});
Network First(网络优先):
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).catch(() => {
return caches.match(event.request);
})
);
});
Stale While Revalidate(先返回缓存,后台更新):
self.addEventListener('fetch', event => {
event.respondWith(
caches.open('v1').then(cache => {
return cache.match(event.request).then(response => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
})
);
});
2. 缓存版本管理
const CACHE_NAME = 'app-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/script/main.js',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
应用层缓存
1. 内存缓存
class MemoryCache {
constructor(maxSize = 100) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (item.expiry && Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.value;
}
set(key, value, ttl) {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
const item = {
value,
expiry: ttl ? Date.now() + ttl : null,
};
this.cache.set(key, item);
}
clear() {
this.cache.clear();
}
}
2. LocalStorage 缓存
class StorageCache {
constructor(prefix = 'cache_') {
this.prefix = prefix;
}
get(key) {
try {
const item = localStorage.getItem(this.prefix + key);
if (!item) return null;
const { value, expiry } = JSON.parse(item);
if (expiry && Date.now() > expiry) {
localStorage.removeItem(this.prefix + key);
return null;
}
return value;
} catch (e) {
return null;
}
}
set(key, value, ttl) {
try {
const item = {
value,
expiry: ttl ? Date.now() + ttl : null,
};
localStorage.setItem(this.prefix + key, JSON.stringify(item));
} catch (e) {
// 存储空间不足
console.error('Storage cache failed:', e);
}
}
remove(key) {
localStorage.removeItem(this.prefix + key);
}
clear() {
Object.keys(localStorage).forEach(key => {
if (key.startsWith(this.prefix)) {
localStorage.removeItem(key);
}
});
}
}
缓存最佳实践
1. 缓存策略选择
| 资源类型 | 缓存策略 | Cache-Control |
|---|---|---|
| HTML | 不缓存或短缓存 | no-cache 或 max-age=0 |
| CSS/JS | 长期缓存 + 版本号 | max-age=31536000, immutable |
| 图片 | 长期缓存 | max-age=31536000 |
| API | 短缓存 | max-age=300 |
| 字体 | 长期缓存 | max-age=31536000 |
2. 版本控制
文件名版本号:
// webpack
output: {
filename: '[name].[contenthash:8].js',
}
// vite
build: {
rollupOptions: {
output: {
entryFileNames: 'js/[name]-[hash].js',
},
},
}
查询参数版本号:
<link rel="stylesheet" href="style.css?v=1.0.0" />
3. 缓存更新
强制更新:
// 添加版本号或时间戳
const url = `/api/data?v=${Date.now()}`;
Service Worker 更新:
// 检查更新
navigator.serviceWorker.register('/sw.js').then(registration => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 新版本可用,提示用户刷新
}
});
});
});
常见问题
1. 缓存不生效
原因:
- Cache-Control 配置错误
- 文件名没有版本号
- 浏览器禁用缓存
解决:
- 检查 HTTP 响应头
- 使用版本号或 hash
- 清除浏览器缓存测试
2. 缓存更新不及时
原因:
- 强缓存时间过长
- 没有版本控制
解决:
- 缩短缓存时间
- 使用版本号或 hash
- 使用协商缓存
3. 缓存占用空间过大
解决:
- 限制缓存大小
- 定期清理过期缓存
- 使用 LRU 策略