缓存策略的艺术
在工作中,我经常遇到页面加载缓慢的问题。经过分析发现,很多性能瓶颈都与缓存策略不当有关。
静态资源缓存
对于不经常变动的静态资源,设置合适的缓存头是关键:
// 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');
预加载关键资源
使用 preload 和 prefetch 提示浏览器提前加载资源:
<!-- 预加载当前页面关键资源 -->
<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 秒:
- 将 20 个商品图片请求合并为 1 个雪碧图请求
- 实现路由级代码分割,首屏 JS 体积减少 60%
- 添加图片懒加载,初始加载图片数量减少 70%
- 使用 Service Worker 缓存 API 响应
这些优化措施带来的性能提升非常明显,用户留存率提升了 15%。性能优化是一个持续的过程,需要根据具体业务场景不断调整策略。
暂无评论