前端缓存实战:用Service Worker和IndexedDB应对高并发场景
凌晨三点,我被一阵急促的告警声惊醒。手机屏幕上显示:"接口响应超时率超过80%,网关持续熔断。"
这不是服务器故障,也不是数据库问题,而是用户量突然暴增带来的连锁反应。某个功能被大规模使用,海量请求涌向几个核心接口,形成了调用风暴。
后端团队紧急扩容服务器,但资源有限,响应速度依然缓慢。更糟糕的是,用户开始频繁刷新页面,每次刷新都产生新的请求,形成了恶性循环。
我们意识到:单纯依靠后端已经无法应对这种情况。
有没有办法在用户端拦截重复请求,用本地缓存来分担压力?不改动接口、不调整架构、不影响现有业务逻辑。
答案是:Service Worker + IndexedDB。
我们通过独立部署的sw.js文件,在浏览器底层拦截高频请求。命中的请求直接返回缓存结果,未命中的才走网络请求。整个过程用户无感知,业务代码无需修改。
上线后,关键接口的外部请求量下降70%,页面加载速度大幅提升,系统告警解除。
这不是技术炫技,而是一次真实的前端"反向兜底"实践。下面分享这套方案的完整细节。
核心策略:区分请求类型处理
1. GET请求使用Cache Storage
对于行情主表这类GET接口,使用Cache Storage是最佳选择:
self.addEventListener('fetch', function(event) {
const url = event.request.url;
if (url.includes('finance/stock/maintable/index_merge')) {
handleCache(event, 'STOCK_MAIN', 60 * 1000); // 缓存1分钟
}
});缓存处理逻辑:
function handleCache(event, cacheName, maxAge) {
event.respondWith(
caches.match(event.request).then(cached => {
// 如果缓存存在且未过期,直接返回
if (cached) {
const cachedTime = new Date(cached.headers.get('Date')).getTime();
if (Date.now() - cachedTime < maxAge) {
return cached;
}
}
// 缓存不存在或已过期,走网络请求
return fetch(event.request).then(response => {
if (response.ok) {
const clone = response.clone();
caches.open(cacheName).then(cache => {
cache.put(event.request, clone);
});
}
return response;
});
})
);
}优点:浏览器原生支持,性能优秀,适合幂等性GET请求。
2. POST请求使用IndexedDB
很多高频接口是POST请求,比如指标计算、文本分析等。这些请求参数不同,结果也不同。
Cache Storage只支持GET请求,这时候就需要IndexedDB:
if (event.request.method === 'POST' && url.includes('statistic/component/ptext')) {
handlePostCache(event, 'POST_TEXT_CACHE');
}POST请求缓存处理:
function handlePostCache(event, storeName) {
event.respondWith((async function() {
const request = event.request.clone();
const body = await request.text(); // 使用请求体作为缓存键
// 尝试从缓存获取
const cached = await idbGet(storeName, body);
if (cached) {
return new Response(cached.body, cached.options);
}
// 缓存未命中,发起网络请求
const response = await fetch(event.request.clone());
if (response.ok) {
const responseBody = await response.clone().text();
const options = {
status: response.status,
statusText: response.statusText,
headers: Array.from(response.headers.entries())
};
// 缓存响应结果
await idbSet(storeName, body, {
body: responseBody,
options: options
});
}
return response;
})());
}关键设计:
使用请求体内容作为缓存键,确保不同参数返回不同结果
缓存响应头和状态码,完整还原接口行为
异步操作,不阻塞主线程
3. IndexedDB轻量封装
提供简单的读写封装:
function idbGet(store, key) {
return new Promise((resolve, reject) => {
const open = indexedDB.open('sw-cache-db', 1);
open.onupgradeneeded = function(e) {
const db = e.target.result;
if (!db.objectStoreNames.contains(store)) {
db.createObjectStore(store);
}
};
open.onsuccess = function(e) {
const db = e.target.result;
const tx = db.transaction(store, 'readonly');
const req = tx.objectStore(store).get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
};
open.onerror = () => reject(open.error);
});
}
function idbSet(store, key, value) {
return new Promise((resolve, reject) => {
const open = indexedDB.open('sw-cache-db', 1);
open.onsuccess = function(e) {
const db = e.target.result;
const tx = db.transaction(store, 'readwrite');
tx.objectStore(store).put(value, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
};
open.onerror = () => reject(open.error);
});
}页面注册:简单接入
在html页面中添加注册代码:
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('Service Worker注册成功');
})
.catch(function(error) {
console.log('Service Worker注册失败:', error);
});
});
}
</script>实际效果
接口平均响应时间:从800ms降低到80ms
后端请求压力:高峰期减少65%
用户刷新体验:接近本地应用速度
代码改动:业务代码零侵入,只增加sw.js文件
使用原则和最佳实践
1. 选择合适的缓存接口
高频访问的接口
数据时效性要求不高的接口
静态配置、公告信息等
2. 设置合理的缓存策略
// 不同接口设置不同的缓存时间
const cacheConfig = {
'stock/data': 60 * 1000, // 1分钟
'news/list': 5 * 60 * 1000, // 5分钟
'config/static': 24 * 60 * 60 * 1000 // 24小时
};3. 控制缓存容量
定期清理过期缓存,防止存储空间无限增长:
// 定期清理过期缓存
function cleanExpiredCache() {
// 清理逻辑
}4. 监控缓存效果
使用Chrome开发者工具监控:
Application → Cache Storage
Application → IndexedDB
Network面板查看请求命中情况
缓存更新策略
1. 版本控制
通过修改Service Worker版本号触发更新:
const CACHE_VERSION = 'v1.2.0';
const CACHE_NAME = `app-cache-${CACHE_VERSION}`;2. 增量更新
只更新变化的资源,减少数据传输:
// 检查资源是否需要更新
function needsUpdate(cachedResponse, networkResponse) {
// 比较ETag或Last-Modified
return cachedResponse.headers.get('etag') !==
networkResponse.headers.get('etag');
}错误处理和降级方案
1. 网络异常处理
function handleCache(event, cacheName, maxAge) {
event.respondWith(
caches.match(event.request).then(cached => {
// ...缓存逻辑
}).catch(error => {
// 降级到网络请求
return fetch(event.request);
})
);
}2. 缓存失败处理
async function handlePostCache(event, storeName) {
try {
// 缓存处理逻辑
} catch (error) {
// 直接返回网络请求
return fetch(event.request);
}
}总结
面对高并发场景,前端不再只能被动等待后端扩容。使用Service Worker和IndexedDB的组合方案,可以主动分担系统压力,提升用户体验。
这次实战经验告诉我们:前端完全可以成为系统稳定性的重要保障。
如果你的项目面临以下挑战:
高频接口调用
弱网络环境
用户体验瓶颈
建议尝试这个技术组合:Service Worker + Cache Storage + IndexedDB。
这个方案经过实际验证,在保证数据准确性的同时,显著提升了系统性能和用户体验。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!