前端缓存策略实战:从 localStorage 到 Service Worker
常见误区
很多开发者对缓存存在误解,认为这只是浏览器自动处理的事。比如直接禁用缓存省麻烦,结果每次都要重新加载,浪费带宽;或者以为缓存就是 localStorage,管理混乱导致内存占用高。醒醒吧,前端缓存不是简单的本地存储,而是一套完整的策略体系。
为什么需要它?
合理的缓存策略能带来实实在在的好处:减少重复请求以加快页面加载,支持离线访问提升用户体验,节省服务器流量降低成本,甚至在网络不稳定时保证应用可用性。
反面教材
看看这些典型的错误写法,它们往往会导致性能问题或数据不一致。
// 反面教材:滥用 localStorage,没有失效机制
function fetchData() {
// 每次都从 API 获取数据
return fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => {
// 直接存储到 localStorage,不考虑过期
localStorage.setItem('data', JSON.stringify(data));
return data;
});
}
// 反面教材:永远使用缓存,不考虑过期
function getCachedData() {
const cachedData = localStorage.getItem('data');
return cachedData ? JSON.parse(cachedData) : null;
}
// 反面教材:缓存键命名混乱
function cacheData(key, data) {
// 缓存键没有统一规范,难以维护
localStorage.setItem(`cache_${key}_${Date.now()}`, JSON.stringify(data));
}
正确的做法
我们需要构建一个完整的缓存管理系统,包含过期策略、空间管理和统一的命名规范。
1. 封装 CacheManager
这个类负责处理数据的增删改查以及过期判断。
class CacheManager {
constructor() {
this.cachePrefix = 'app_cache_';
this.defaultExpiry = 24 * 60 * 60 * 1000; // 默认 24 小时
}
generateKey(key) {
return `${this.cachePrefix}${key}`;
}
set(key, data, expiry = this.defaultExpiry) {
const cacheItem = {
data,
expiry: Date.now() + expiry,
timestamp: Date.now()
};
try {
localStorage.setItem(this.generateKey(key), JSON.stringify(cacheItem));
} catch (error) {
console.error('Cache storage error:', error);
this.clearOldCache();
}
}
get(key) {
const cacheItem = localStorage.getItem(this.generateKey(key));
if (!cacheItem) return null;
try {
const parsedItem = JSON.parse(cacheItem);
// 检查是否过期
if (Date.now() > parsedItem.expiry) {
this.remove(key);
return null;
}
return parsedItem.data;
} catch (error) {
console.error('Cache parsing error:', error);
this.remove(key);
return null;
}
}
remove(key) {
localStorage.removeItem(this.generateKey(key));
}
clear() {
Object.keys(localStorage).forEach(key => {
if (key.startsWith(this.cachePrefix)) {
localStorage.removeItem(key);
}
});
}
clearOldCache() {
Object.keys(localStorage).forEach(key => {
if (key.startsWith(this.cachePrefix)) {
try {
const item = JSON.parse(localStorage.getItem(key));
if (Date.now() > item.expiry) {
localStorage.removeItem(key);
}
} catch (error) {
localStorage.removeItem(key);
}
}
});
}
getCacheSize() {
let size = 0;
Object.keys(localStorage).forEach(key => {
if (key.startsWith(this.cachePrefix)) {
size += localStorage.getItem(key).length;
}
});
return size;
}
}
2. 使用 Service Worker 缓存静态资源
对于静态资源和 API 响应,Service Worker 是更强大的选择,它能拦截网络请求并控制缓存逻辑。
// service-worker.js
const CACHE_NAME = 'app-cache-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/manifest.json',
'/static/js/main.js',
'/static/css/main.css',
'/static/images/logo.png'
];
// 安装 Service Worker
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(ASSETS_TO_CACHE);
})
);
});
// 激活 Service Worker
self.addEventListener('activate', event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
});
// 拦截网络请求
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') {
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
}
return response;
})
)
);
});
3. API 请求缓存封装
结合上面的管理器,我们可以轻松实现带缓存的 API 请求。
async function fetchWithCache(url, options = {}) {
const cacheKey = `api_${url}_${JSON.stringify(options)}`;
const cacheManager = new CacheManager();
// 尝试从缓存获取
const cachedData = cacheManager.get(cacheKey);
if (cachedData) {
return cachedData;
}
// 发起网络请求
const response = await fetch(url, options);
const data = await response.json();
// 缓存数据,5 分钟过期
cacheManager.set(cacheKey, data, 5 * 60 * 1000);
return data;
}
核心建议
记住,缓存不是越多越好,而是要合理使用。你需要根据数据的性质和使用频率,选择合适的缓存策略。不要觉得缓存只是浏览器的事,它是前端性能优化的重要组成部分。
总结
- localStorage:适合存储小量、不敏感的数据
- sessionStorage:适合存储会话期间的数据
- IndexedDB:适合存储大量结构化数据
- Service Worker:适合缓存静态资源和 API 响应
- 缓存策略:合理设置过期时间,定期清理过期缓存
- 缓存键管理:使用统一的命名规范,避免缓存键冲突
- 错误处理:处理存储空间不足等异常情况
- 性能监控:监控缓存命中率,不断优化缓存策略
