缓存策略全解析与前端性能优化实战经验分享
我的写法,亲测靠谱
先说说我常用的缓存策略。在实际项目中,我一般会结合 HTTP 缓存和 Service Worker 来做,这样能覆盖大部分场景。HTTP 缓存用来处理静态资源,Service Worker 用来管理动态数据。
比如对于静态资源,我会这样设置响应头:
app.get('/static/*', (req, res) => {
const filePath = path.join(__dirname, 'public', req.path);
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
res.sendFile(filePath);
});
这里用 immutable 是个关键点。它告诉浏览器只要文件名不变,就别重新请求了。我之前踩过坑,没加这个属性,结果每次发布新版本时,用户端还是会去验证缓存,白白浪费请求。
这几种错误写法,别再踩坑了
说到踩坑,还真不少。最常见的就是滥用 no-cache。很多人以为加上这个就能防止缓存,其实它只是强制每次都要验证而已。正确的做法应该是:
res.setHeader('Cache-Control', 'no-store');
还有就是 ETag 的使用问题。有人喜欢给所有接口都加上 ETag,觉得这样很安全。但实际上,ETag 在分布式系统里很容易出问题,不同服务器生成的 ETag 可能不一致。我就遇到过一个 case,同样的资源,在负载均衡后的不同机器上返回的 ETag 不一样,导致缓存完全失效。
另外要特别提醒的是,千万别在 HTTPS 下返回不安全的缓存配置。比如这样写就很危险:
res.setHeader('Cache-Control', 'public, max-age=86400');
如果页面包含敏感信息,这种写法可能会导致隐私泄露。我建议至少加上 private 标志。
实际项目中的坑
在最近一个电商项目里,我们遇到了个头疼的问题。商品详情页的数据经常变化,但又不能每次都去请求后端接口。最开始我们用了简单的内存缓存:
const cache = {};
app.get('/api/product/:id', async (req, res) => {
const { id } = req.params;
if (cache[id] && Date.now() - cache[id].timestamp < 60000) {
return res.json(cache[id].data);
}
const data = await fetchProduct(id);
cache[id] = { data, timestamp: Date.now() };
res.json(data);
});
看起来没问题对吧?结果上线后发现,高峰期内存暴涨,GC 频繁触发,服务直接卡死了。后来改成用 LRU 算法限制缓存大小:
const LRU = require('lru-cache');
const cache = new LRU({ max: 500 });
app.get('/api/product/:id', async (req, res) => {
const { id } = req.params;
if (cache.has(id)) {
return res.json(cache.get(id));
}
const data = await fetchProduct(id);
cache.set(id, data);
res.json(data);
});
这才算稳定下来。所以说啊,缓存看着简单,真要做好还挺费劲的。
Service Worker 的那些事儿
再说说 Service Worker。这玩意儿确实强大,但也容易踩坑。最基本的写法是这样的:
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request).then(res => {
return caches.open('dynamic').then(cache => {
cache.put(event.request, res.clone());
return res;
});
});
})
);
});
但是这里有个坑:如果你更新了 Service Worker 文件,旧版本可能还在运行,导致用户看到的是老内容。解决办法是在注册时加上版本号:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js?v=2')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW failed'));
}
每更新一次就改版本号,确保新代码能生效。不过这也带来个新问题:怎么清理旧版本的缓存?我的做法是在 activate 事件里清理:
self.addEventListener('activate', event => {
const currentCaches = ['dynamic-v2'];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (!currentCaches.includes(cacheName)) {
return caches.delete(cacheName);
}
})
);
})
);
});
结尾唠叨几句
以上是我个人对缓存策略的一些实战总结。说实话,缓存这东西没有银弹,每个项目都有自己的特点。像我前阵子做的一个报表系统,因为数据实时性要求高,最后反而基本没用缓存。
这里分享的经验都是踩过坑后的教训,希望能帮你少走弯路。当然,技术总是在发展,说不定哪天又会有更好的方案出现。有更优的实现方式欢迎评论区交流,咱们共同进步。

暂无评论