一、为什么我们需要缓存策略

想象一下你每天都要去同一家咖啡店买咖啡,店员每次都要重新确认你的口味和支付方式,是不是很烦?如果店员能记住你的偏好,效率就会高很多。前端缓存也是同样的道理——通过存储重复使用的资源,让应用跑得更快更顺。

在单页面应用(SPA)或数据驱动的Web应用中,频繁的API调用和资源加载会拖慢性能。比如一个电商网站的商品列表,如果用户每次切换分类都要重新请求数据,体验就会非常糟糕。这时候合理的缓存策略就是你的救命稻草。

二、浏览器自带的缓存机制

1. 强缓存:Cache-Control与Expires

浏览器最基础的缓存能力,通过HTTP响应头控制。比如设置Cache-Control: max-age=3600表示资源1小时内不会重新请求。

// 示例:Node.js Express设置缓存头(技术栈:Node.js)
app.get('/static/logo.png', (req, res) => {
  res.setHeader('Cache-Control', 'public, max-age=86400'); // 缓存1天
  res.sendFile(path.join(__dirname, 'assets/logo.png'));
});

注意:强缓存期间即使服务器资源更新,客户端也不会获取新版本,适合不变的静态资源。

2. 协商缓存:Last-Modified与ETag

当缓存过期时,浏览器会带着If-Modified-SinceIf-None-Match头询问服务器资源是否变更。若未变更则返回304状态码,继续使用本地缓存。

// 示例:Koa实现ETag验证(技术栈:Node.js)
app.use(async (ctx) => {
  const file = await fs.readFile('data.json');
  const etag = crypto.createHash('md5').update(file).digest('hex');
  if (ctx.headers['if-none-match'] === etag) {
    ctx.status = 304; // 资源未修改
  } else {
    ctx.set('ETag', etag);
    ctx.body = file;
  }
});

三、手动缓存的高级玩法

1. 内存缓存:用Map实现极速读取

适合短期存储高频使用的数据,比如用户权限信息。

// 示例:内存缓存类(技术栈:JavaScript)
class MemoryCache {
  constructor() {
    this.cache = new Map();
    this.timer = new Map();
  }

  set(key, value, ttl = 5000) {
    this.cache.set(key, value);
    clearTimeout(this.timer.get(key)); // 清除旧定时器
    this.timer.set(key, setTimeout(() => this.delete(key), ttl));
  }

  get(key) {
    return this.cache.get(key);
  }
}

// 使用示例
const userCache = new MemoryCache();
userCache.set('user_123', { name: '张三' }, 30000); // 缓存30秒

2. localStorage持久化缓存

需要长期存储且数据量小于5MB时的选择,比如用户个性化设置。

// 示例:带过期时间的localStorage封装(技术栈:JavaScript)
const storage = {
  set(key, value, expireDays) {
    const data = { 
      value,
      expire: Date.now() + expireDays * 86400000 
    };
    localStorage.setItem(key, JSON.stringify(data));
  },
  get(key) {
    const data = JSON.parse(localStorage.getItem(key));
    if (data && data.expire > Date.now()) {
      return data.value;
    }
    localStorage.removeItem(key);
    return null;
  }
};

// 存储主题配置(有效期7天)
storage.set('theme_config', { darkMode: true }, 7);

四、实战中的缓存策略组合

场景1:API响应缓存

对于实时性要求不高的数据(如新闻列表),可以采用内存缓存+过期验证策略:

// 示例:Axios拦截器实现API缓存(技术栈:JavaScript)
const apiCache = new Map();

axios.interceptors.request.use(config => {
  if (config.cache) {
    const key = JSON.stringify(config);
    if (apiCache.has(key)) {
      const { expire, data } = apiCache.get(key);
      if (expire > Date.now()) return Promise.resolve(data);
    }
  }
  return config;
});

axios.interceptors.response.use(response => {
  if (response.config.cache) {
    const key = JSON.stringify(response.config);
    apiCache.set(key, {
      data: response,
      expire: Date.now() + (response.config.cacheTTL || 30000)
    });
  }
  return response;
});

// 使用示例:缓存30秒的请求
axios.get('/api/news', { cache: true, cacheTTL: 30000 });

场景2:图片懒加载与缓存

结合Intersection Observer API实现图片按需加载并缓存:

// 示例:图片懒加载组件(技术栈:JavaScript)
class LazyImage {
  constructor(selector) {
    this.images = document.querySelectorAll(selector);
    this.observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src; // 触发实际加载
          this.observer.unobserve(img); // 停止观察
        }
      });
    });
  }

  start() {
    this.images.forEach(img => this.observer.observe(img));
  }
}

// 初始化观察所有带data-src的图片
new LazyImage('img[data-src]').start();

五、缓存策略的雷区与避坑指南

  1. 内存泄漏

    • 定时器未清除会导致缓存对象无法被垃圾回收
    • 解决方案:使用WeakMap存储短期缓存
  2. 版本控制

    • 静态资源更新后需要更改文件名或添加查询参数(如main.js?v=2
  3. 敏感数据

    • 切勿将token等敏感信息存入localStorage
    • 解决方案:使用HttpOnly的Cookie配合短期内存缓存
  4. 缓存雪崩

    • 大量缓存同时失效导致瞬间高负载
    • 解决方案:为过期时间添加随机偏移量
// 良好的过期时间设置示例
const getRandomExpire = (base, range) => 
  base + Math.floor(Math.random() * range);

// 设置30秒基础过期时间±5秒随机偏移
cache.set('key', data, getRandomExpire(30000, 5000));

六、新时代的缓存方案展望

随着Service Worker和Cache API的普及,离线优先(Offline-First)成为可能:

// 示例:Service Worker缓存策略(技术栈:JavaScript)
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cached => cached || fetch(event.request))
      .then(response => {
        // 缓存GET请求的响应
        if (event.request.method === 'GET') {
          const clone = response.clone();
          caches.open('v1').then(cache => cache.put(event.request, clone));
        }
        return response;
      })
  );
});

这种方案特别适合PWA应用,即使网络不稳定也能保证基本功能可用。

总结

好的缓存策略就像给应用装上了涡轮增压——内存缓存是氮气加速,localStorage是超大油箱,Service Worker则是全时四驱系统。但记住没有银弹,需要根据数据敏感性、实时性要求和设备条件灵活组合策略。下次当你发现页面加载缓慢时,不妨先问自己:这里该用什么缓存?