158 lines
4.5 KiB
Markdown
158 lines
4.5 KiB
Markdown
|
|
---
|
|||
|
|
tags: [React, Performance, Optimization, Frontend]
|
|||
|
|
create time: 2026-04-29 22:14
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 性能优化
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
React 性能优化的核心原则是 **"减少不必要的渲染"**。本文档从诊断工具到具体手段,提供完整的调优方法论。
|
|||
|
|
|
|||
|
|
## 性能诊断工具箱
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TB
|
|||
|
|
A[发现性能问题] --> B["选择诊断工具"]
|
|||
|
|
|
|||
|
|
B --> C["React DevTools Profiler"]
|
|||
|
|
B --> D["Chrome Performance Tab"]
|
|||
|
|
B --> E["Lighthouse"]
|
|||
|
|
B --> F["Web Vitals 监控"]
|
|||
|
|
|
|||
|
|
C --> G["定位重渲染的组件和原因"]
|
|||
|
|
D --> H["分析主线程阻塞时段"]
|
|||
|
|
E --> I["整体 LCP/FID/CLS 评分"]
|
|||
|
|
F --> J["生产环境真实用户数据"]
|
|||
|
|
|
|||
|
|
style C fill:#61DAFB,color:#000
|
|||
|
|
style H fill:#F5A87D,color:#000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### React DevTools Profiler 使用要点
|
|||
|
|
|
|||
|
|
1. 录制期间进行关键交互(点击、输入)
|
|||
|
|
2. 观察 **Commit 颜色** —— 红色越深表示重渲染越多
|
|||
|
|
3. 展开组件树,关注 "why did this render" 原因
|
|||
|
|
4. 对比优化前后的 Commit 时间变化
|
|||
|
|
|
|||
|
|
## React.memo —— 阻止子组件重渲染
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const ExpensiveList = React.memo(({ items }: { items: Item[] }) => {
|
|||
|
|
// props.items 引用不变时,跳过整个子树的 re-render
|
|||
|
|
return (
|
|||
|
|
<ul>
|
|||
|
|
{items.map(item => <li key={item.id}>{item.name}</li>)}
|
|||
|
|
</ul>
|
|||
|
|
);
|
|||
|
|
}, (prevProps, nextProps) => prevProps.items === nextProps.items); // 自定义比较函数
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!warning] React.memo 的适用边界
|
|||
|
|
> - 只对**纯组件**有用——相同 props 必须产生相同的输出
|
|||
|
|
> - 父组件每次传新对象/函数作 prop → memo 无效
|
|||
|
|
> - 简单列表(几十项以内)不需要 memo
|
|||
|
|
|
|||
|
|
## 虚拟列表(Virtual Scrolling)
|
|||
|
|
|
|||
|
|
当列表项超过数百条时,虚拟滚动通过只渲染可视区域内的 DOM 元素来大幅降低内存占用:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 方案1:tanstack/virtual(推荐)
|
|||
|
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|||
|
|
|
|||
|
|
function VirtualList({ items }: { items: string[] }) {
|
|||
|
|
const parentRef = useRef<HTMLDivElement>(null);
|
|||
|
|
|
|||
|
|
const virtualizer = useVirtualizer({
|
|||
|
|
count: items.length,
|
|||
|
|
getScrollElement: () => parentRef.current,
|
|||
|
|
estimateSize: () => 50, // 预估每项高度
|
|||
|
|
overscan: 5, // 视口上下各多渲染 5 项
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div ref={parentRef} style={{ height: 400, overflow: "auto" }}>
|
|||
|
|
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: "relative" }}>
|
|||
|
|
{virtualizer.getVirtualItems().map(virtualRow => (
|
|||
|
|
<div
|
|||
|
|
key={virtualRow.index}
|
|||
|
|
style={{
|
|||
|
|
position: "absolute",
|
|||
|
|
top: 0,
|
|||
|
|
left: 0,
|
|||
|
|
width: "100%",
|
|||
|
|
height: `${virtualRow.size}px`,
|
|||
|
|
transform: `translateY(${virtualRow.start}px)`,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{items[virtualRow.index]}
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 何时需要虚拟列表?
|
|||
|
|
> - 列表项 > ~50 且每帧渲染耗时可感知
|
|||
|
|
> - 固定高度的项目比可变高度更容易实现
|
|||
|
|
> - React Window / React Virtualized 是老牌的成熟方案
|
|||
|
|
|
|||
|
|
## Code Splitting
|
|||
|
|
|
|||
|
|
### 路由级拆分
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// Next.js App Router(内置)
|
|||
|
|
const AdminPage = lazy(() => import("./pages/Admin"));
|
|||
|
|
|
|||
|
|
// vite + React Router
|
|||
|
|
const Settings = lazy(() => import(/* vite: preload */ "./pages/Settings"));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 组件级拆分
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
import dynamic from "next/dynamic";
|
|||
|
|
|
|||
|
|
// 不加载图表库直到真正需要
|
|||
|
|
const Chart = dynamic(() => import("recharts"), { ssr: false });
|
|||
|
|
// 或带 loading fallback
|
|||
|
|
const HeavyEditor = dynamic(() => import("@monaco-editor/react"), {
|
|||
|
|
loading: () => <p>Loading editor...</p>,
|
|||
|
|
ssr: false,
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Bundle 分析与优化
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 安装插件
|
|||
|
|
npm install --save-dev rollup-plugin-visualizer
|
|||
|
|
# 或在 Next.js 中使用 next-bundle-analyzer
|
|||
|
|
|
|||
|
|
# 生成可视化报告
|
|||
|
|
npx run build && npx visualizer
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] Bundle 大小目标
|
|||
|
|
> | 层级 | 目标大小(gzipped)|
|
|||
|
|
> |------|---------------------|
|
|||
|
|
> | 首屏 chunk | < 150KB |
|
|||
|
|
> | 单个 chunk | < 300KB |
|
|||
|
|
> | JS Total | < 500KB(SPA)/ < 200KB(PWA) |
|
|||
|
|
|
|||
|
|
## Lighthouse 关键指标调优
|
|||
|
|
|
|||
|
|
| 指标 | 含义 | 优化方向 |
|
|||
|
|
|------|------|----------|
|
|||
|
|
| **FCP**(First Contentful Paint) | 首次内容绘制 | 减小首屏 HTML/JS 体积 |
|
|||
|
|
| **LCP**(Largest Contentful Paint) | 最大内容绘制 | 图片懒加载、预加载关键资源 |
|
|||
|
|
| **INP**(Interaction to Next Paint) | 交互响应延迟 | useTransition、删除同步 heavy work |
|
|||
|
|
| **CLS**(Cumulative Layout Shift) | 布局偏移 | 预留图片宽高、避免字体闪烁 |
|
|||
|
|
|
|||
|
|
## 关联笔记
|