缓存策略的艺术

在工作中,我经常遇到页面加载缓慢的问题。经过分析发现,很多性能瓶颈都与缓存策略不当有关。

静态资源缓存

对于不经常变动的静态资源,设置合适的缓存头是关键:

// Express 中设置静态资源缓存
app.use(express.static('public', {
  maxAge: '30d',
  etag: false,
  lastModified: false
}));

我建议将 CSS、JS、图片等资源设置为长期缓存(30天),同时通过文件哈希来实现缓存更新。这样用户在首次访问后,后续访问会直接从缓存加载,大幅提升体验。

API 响应缓存

对于数据接口,合理的缓存策略同样重要。我习惯在客户端实现简单的内存缓存:

class ApiCache {
  constructor() {
    this.cache = new Map();
  }
  
  async get(key, fetchFn, ttl = 60000) {
    const cached = this.cache.get(key);
    if (cached && Date.now() - cached.timestamp < ttl) {
      return cached.data;
    }
    
    const data = await fetchFn();
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });
    return data;
  }
}

代码分割的实践心得

路由级代码分割

在 React 项目中,我习惯使用路由级别的代码分割:

const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </Suspense>
  );
}

这种方式可以确保用户只加载当前页面需要的代码,显著减少初始包大小。

组件级懒加载

对于大型组件,我还会进一步拆分:

const HeavyChart = lazy(() => 
  import('./components/HeavyChart').then(module => ({
    default: module.HeavyChart
  }))
);

// 在需要时再加载
const showChart = async () => {
  await import('./components/HeavyChart');
  setShowChart(true);
};

图片优化技巧

图片格式选择

根据使用场景选择合适的图片格式:

  • WebP:现代浏览器首选,压缩率优秀
  • AVIF:最新的图像格式,压缩效果更好
  • JPEG:适合照片类图片
  • PNG:需要透明背景时使用

响应式图片

使用 srcset 属性提供不同尺寸的图片:

<img 
  src="/images/hero-small.jpg"
  srcset="/images/hero-small.jpg 480w,
          /images/hero-medium.jpg 768w,
          /images/hero-large.jpg 1200w"
  sizes="(max-width: 480px) 480px,
         (max-width: 768px) 768px,
         1200px"
  alt="Hero image"
/>

这种方式可以根据设备屏幕大小加载合适的图片,避免大图小用。

JavaScript 执行优化

避免强制同步布局

在修改样式后立即读取布局属性会导致强制同步布局:

// 不好的写法
function updateWidth() {
  element.style.width = '100px';
  const width = element.offsetWidth; // 强制同步布局
  // ...
}

// 好的写法
function updateWidth() {
  element.style.width = '100px';
  requestAnimationFrame(() => {
    const width = element.offsetWidth;
    // ...
  });
}

使用 Web Workers

对于计算密集型任务,使用 Web Workers 可以避免阻塞主线程:

// 主线程
const worker = new Worker('compute.js');
worker.postMessage({ data: largeData });
worker.onmessage = (event) => {
  console.log('Result:', event.data);
};

// compute.js
self.onmessage = (event) => {
  const result = heavyComputation(event.data);
  self.postMessage(result);
};

网络请求优化

请求合并

对于多个小请求,可以考虑合并:

// 合并前
await fetch('/api/user/1');
await fetch('/api/user/2');
await fetch('/api/user/3');

// 合并后
await fetch('/api/users?ids=1,2,3');

预加载关键资源

使用 preloadprefetch 提示浏览器提前加载资源:

<!-- 预加载当前页面关键资源 -->
<link rel="preload" href="critical.css" as="style">
<link rel="preload" href="main.js" as="script">

<!-- 预获取下一页可能用到的资源 -->
<link rel="prefetch" href="next-page.js">

实际案例分析

最近优化了一个电商列表页,原始加载时间约 3.5 秒。通过以下措施优化到了 1.2 秒:

  1. 将 20 个商品图片请求合并为 1 个雪碧图请求
  2. 实现路由级代码分割,首屏 JS 体积减少 60%
  3. 添加图片懒加载,初始加载图片数量减少 70%
  4. 使用 Service Worker 缓存 API 响应

这些优化措施带来的性能提升非常明显,用户留存率提升了 15%。性能优化是一个持续的过程,需要根据具体业务场景不断调整策略。