前言
性能优化不是"感觉很快",而是可度量的。本文以 Lighthouse 报告为起点,拆解从构建到运行时的完整优化链路,每一步都配有可落地的代码和配置。
一、读懂 Lighthouse 报告
Lighthouse 六大核心指标:
| 指标 | 含义 | 优秀阈值 |
|---|---|---|
| FCP (First Contentful Paint) | 首次内容绘制 | < 1.8s |
| LCP (Largest Contentful Paint) | 最大内容绘制 | < 2.5s |
| TBT (Total Blocking Time) | 总阻塞时间 | < 200ms |
| CLS (Cumulative Layout Shift) | 累计布局偏移 | < 0.1 |
| Speed Index | 速度指数 | < 3.4s |
| TTI (Time to Interactive) | 可交互时间 | < 3.8s |
二、构建阶段优化
2.1 Tree Shaking
// 确保 package.json 中声明 sideEffects
{
"name": "my-lib",
"sideEffects": false // 或 ["*.css"] 如果有副作用文件
}
// Vite 默认开启,Webpack 需配置 mode: 'production'
2.2 Code Splitting(代码分割)
// React 路由级别懒加载
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Vite 手动分割配置
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-vendor': ['antd', '@ant-design/icons'],
}
}
}
}
});
2.3 图片优化
// Vite 配置自动压缩
import viteImagemin from 'vite-plugin-imagemin';
export default defineConfig({
plugins: [
viteImagemin({
gifsicle: { optimizationLevel: 3 },
mozjpeg: { quality: 80 },
pngquant: { quality: [0.7, 0.8] },
svgo: { plugins: [{ removeViewBox: false }] }
})
]
});
// 使用现代图片格式
<picture>
<source srcSet="hero.avif" type="image/avif" />
<source srcSet="hero.webp" type="image/webp" />
<img src="hero.jpg" alt="hero" loading="lazy" />
</picture>
三、网络阶段优化
3.1 资源预加载策略
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://api.example.com" />
<!-- 预连接(DNS + TCP + TLS)-->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<!-- 关键资源预加载 -->
<link rel="preload" href="/fonts/main.woff2" as="font" crossorigin />
<!-- 预获取下一页资源 -->
<link rel="prefetch" href="/page-2.js" as="script" />
3.2 缓存策略
# Nginx 静态资源缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML 不缓存
location / {
add_header Cache-Control "no-cache";
}
四、渲染阶段优化
4.1 虚拟列表
// 万级列表渲染方案
function VirtualList({ items, itemHeight, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 1,
items.length
);
const visibleItems = items.slice(startIndex, endIndex);
return (
<div style={{ height: containerHeight, overflow: 'auto' }}
onScroll={e => setScrollTop(e.target.scrollTop)}>
<div style={{ height: items.length * itemHeight, position: 'relative' }}>
{visibleItems.map((item, i) => (
<div key={startIndex + i}
style={{ position: 'absolute', top: (startIndex + i) * itemHeight, height: itemHeight }}>
{item}
</div>
))}
</div>
</div>
);
}
4.2 图片懒加载
<!-- 原生懒加载,兼容率 93%+ -->
<img src="placeholder.jpg" data-src="real.jpg" loading="lazy" />
<!-- Intersection Observer 精确控制 -->
<script>
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
</script>
五、运行时性能优化
5.1 React 渲染优化
// useMemo — 缓存计算结果
const sortedList = useMemo(
() => items.sort((a, b) => a.name.localeCompare(b.name)),
[items] // 仅 items 变化时重新计算
);
// useCallback — 稳定函数引用
const handleClick = useCallback((id: number) => {
setSelectedId(id);
}, []); // 不依赖外部变量,引用永远不变
// React.memo — 避免无关渲染
const ExpensiveComponent = React.memo(function ExpensiveComponent({ data }) {
return <div>{/* 复杂渲染 */}</div>;
});
// 使用 useDeferredValue 降低更新优先级
import { useDeferredValue } from 'react';
const deferredQuery = useDeferredValue(searchQuery);
// 用 deferredQuery 渲染列表,searchQuery 更新输入框
5.2 Web Worker 释放主线程
// worker.ts
self.onmessage = (e: MessageEvent<number[]>) => {
const result = e.data
.filter(n => n > 1000)
.map(n => n * 2)
.reduce((a, b) => a + b, 0);
self.postMessage(result);
};
// main.ts
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(largeArray);
worker.onmessage = (e) => console.log('结果:', e.data);
优化效果检查清单
| 优化项 | 方法 | 预期收益 |
|---|---|---|
| Bundle 体积 | Tree Shaking + Code Split | 首屏 JS -40%~60% |
| 图片加载 | WebP/AVIF + 懒加载 + CDN | LCP -30%~50% |
| 字体加载 | font-display: swap + preload | 消除 FOIT/FOUT |
| 长列表 | 虚拟列表 | 内存 -90%,FPS 稳定 60 |
| 重复渲染 | React.memo + useMemo | TBT -50%~70% |
性能优化的黄金法则:先测量,再优化,再测量。不要凭感觉优化,Chrome DevTools Performance 面板是你最好的朋友。