缓存策略全解析与前端性能优化实战经验分享

IT人梓玥 优化 阅读 2,611
赞 21 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

先说说我常用的缓存策略。在实际项目中,我一般会结合 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);
          }
        })
      );
    })
  );
});

结尾唠叨几句

以上是我个人对缓存策略的一些实战总结。说实话,缓存这东西没有银弹,每个项目都有自己的特点。像我前阵子做的一个报表系统,因为数据实时性要求高,最后反而基本没用缓存。

这里分享的经验都是踩过坑后的教训,希望能帮你少走弯路。当然,技术总是在发展,说不定哪天又会有更好的方案出现。有更优的实现方式欢迎评论区交流,咱们共同进步。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论