前端缓存策略详解:从 localStorage 到 Service Worker
常见误区
很多开发者认为'浏览器会自动处理缓存',或者觉得'缓存就是 localStorage 嘛'。结果往往是网站加载慢、内存占用高,甚至因为禁用缓存导致带宽浪费。
前端缓存不是简单的存储键值对,而是一套完整的策略体系。我们需要根据数据性质和使用频率,选择合适的方案。
为什么要做缓存?
合理的缓存策略能带来直接收益:
- 性能提升:减少重复请求,显著加快页面加载速度。
- 用户体验:支持离线访问,减少等待时间。
- 成本节省:降低服务器流量消耗。
- 可靠性:网络波动时仍能正常展示内容。
避坑指南:常见的错误写法
直接操作 localStorage 而不加管理是新手常犯的错误。比如没有过期策略,导致脏数据永远存在;或者缓存键命名混乱,引发冲突。
// 错误示范:没有缓存失效策略
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));
}
构建专业的缓存管理器
为了规范化管理,建议封装一个统一的缓存类。它需要处理过期时间、存储空间不足异常以及清理逻辑。
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);
}
}
});
}
}
进阶:使用 Service Worker 拦截网络请求
对于静态资源和 API 响应,仅靠 localStorage 不够,需要 Service Worker 在底层拦截请求。这能实现更细粒度的控制,比如离线优先(Offline First)。
// 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'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(ASSETS_TO_CACHE);
})
);
});
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;
});
})
);
});
结合上面的 CacheManager,我们可以轻松实现 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();
cacheManager.set(cacheKey, data, 5 * 60 * 1000); // 5 分钟过期
return data;
}
选型建议与总结
不同的场景适合不同的存储方案,不要盲目堆砌技术。
- localStorage:适合小量、不敏感的数据,注意容量限制(通常 5MB)。
- sessionStorage:适合会话期间的临时数据,关闭标签页即清除。
- IndexedDB:适合存储大量结构化数据,支持事务和索引。
- Service Worker:适合缓存静态资源和 API 响应,实现离线能力。
核心原则是合理设置过期时间,定期清理,并统一缓存键命名规范。做好错误处理,监控缓存命中率,才能持续优化性能。

