性能优化
React 默认渲染模型:当组件状态变化时,它和它所有子组件都会重新渲染。大多数情况下这不是问题,但大列表、高频更新场景需要优化。
优化原则
💡 先让应用可运行,再优化。不要过早优化——React 的默认渲染已经足够快,大多数应用无需手动优化。只在确实卡顿时分析、定位、优化。
| 步骤 | 工具 |
|---|---|
| 1. 定位性能瓶颈 | React DevTools Profiler |
| 2. 确认具体问题 | 是渲染太多?计算太重?还是网络慢? |
| 3. 针对性优化 | 跳过渲染、缓存计算、代码分割 |
React.memo — 跳过不必要的渲染
const ExpensiveChild = React.memo(function ExpensiveChild({ name, age }) {
console.log('ExpensiveChild 渲染');
return <div>{name}: {age}</div>;
});
// 当 name 和 age 都不变时,跳过渲染memo 的工作原理
父组件重渲染
↓ React 想要重渲染子组件
React.memo 拦截
↓ 浅比较新旧 props
props 相同?
├── YES → 跳过渲染 ✅
└── NO → 正常渲染自定义比较函数
const areEqual = (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
};
export default React.memo(UserCard, areEqual);🚨 陷阱:不要把所有组件都包上
memo。只有当组件符合以下条件时:
- 组件渲染成本高(有大量 DOM 输出)
- 在同一父组件下频繁渲染
- 传入的 props 在大部分渲染中不变
为什么 memo 常失效
// ❌ memo 无效:每次渲染传入新对象
<MemoChild style={{ color: 'red' }} />
<MemoChild onClick={() => doSomething()} />
// ✅ 稳定引用
const style = useMemo(() => ({ color: 'red' }), []);
const onClick = useCallback(() => doSomething(), []);
<MemoChild style={style} onClick={onClick} />useMemo & useCallback
// useMemo:缓存值(避免重复计算)
const filtered = useMemo(
() => items.filter(i => i.active).sort(byPriority),
[items]
);
// useCallback:缓存函数引用(配合 memo 使用)
const handleSelect = useCallback((id) => {
setSelected(id);
}, []);性能决策表
| 场景 | 是否需要 |
|---|---|
items.filter().sort() — items 长度 < 100 |
❌ 不需要 |
| 同上 — items 长度 > 10000 | ✅ 用 useMemo |
a + b — 加法 |
❌ 不需要 |
传给普通 <div> 的事件处理函数 |
❌ 没必要用 useCallback |
传给 memo(Child) 的事件处理函数且 Child 渲染代价大 |
✅ 用 useCallback |
| 作为 useEffect 的依赖 | ✅ 用 useCallback/useMemo |
💡 最佳实践:useMemo/useCallback 本身也有开销(缓存存储、依赖数组比较)。只在实际有益时使用。
代码分割(Code Splitting)
路由级分割
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}条件分割(按需加载)
const MarkdownEditor = lazy(() => import('./MarkdownEditor'));
function App() {
const [showEditor, setShowEditor] = useState(false);
return (
<div>
<button onClick={() => setShowEditor(true)}>编辑</button>
{showEditor && (
<Suspense fallback={<Loading />}>
<MarkdownEditor />
</Suspense>
)}
</div>
);
}lazy 的限制
- 只能用默认导出(
export default)—— 命名导出需做中间层 - 不能用在服务端渲染中(需要 Next.js 的
next/dynamic)
虚拟列表(Windowing)
当列表项成千上万时,只渲染可见区域的 DOM:
npm install @tanstack/react-virtualimport { useVirtualizer } from '@tanstack/react-virtual';
function BigList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map(vItem => (
<div
key={vItem.key}
style={{
position: 'absolute',
top: 0,
transform: `translateY(${vItem.start}px)`,
}}
>
{items[vItem.index].name}
</div>
))}
</div>
</div>
);
}| 库 | 大小 | 特点 |
|---|---|---|
@tanstack/react-virtual |
~5KB | 无头库,灵活,支持动态尺寸 |
react-window |
~6KB | API 更简,适合固定高度 |
react-virtuoso |
~9KB | 最开箱即用,自动处理动态高度 |
React DevTools Profiler
使用步骤
- 打开 DevTools → Profiler 面板
- 点击录制按钮
- 操作应用(点击、输入等)
- 停止录制
- 分析火焰图和排名
关键指标
火焰图颜色含义:
- 灰色:组件未渲染(被跳过)
- 绿色 → 黄色 → 红色:渲染耗时由低到高
渲染次数(Commit):
- 每次状态更新 = 一次 Commit
- 关注高频 Commit(如连续输入触发十几次)避免的常见模式
1. 在渲染中创建组件
// ❌ 每次渲染创建新组件 → React 视为完全不同类型 → 卸载+重新挂载
function Parent() {
function SlowChild() { return <ExpensiveStuff />; }
return <SlowChild />;
}
// ✅ 在外部定义或使用 useCallback
function SlowChild() { return <ExpensiveStuff />; }
function Parent() { return <SlowChild />; }2. Context 值不当导致全局重渲染
// ❌ 每次渲染 value 都是新对象 → 所有消费者重渲染
<Context.Provider value={{ theme, user }}>
{children}
</Context.Provider>
// ✅ 用 useMemo 稳定 value
const value = useMemo(() => ({ theme, user }), [theme, user]);
<Context.Provider value={value}>{children}</Context.Provider>3. 滥用 React.memo
// ❌ 没必要:原生 HTML 元素不需要 memo
const MemoDiv = React.memo(({ children }) => <div>{children}</div>);Web Vitals 指标
| 指标 | 含义 | 目标 |
|---|---|---|
| FCP (First Contentful Paint) | 首次有内容渲染 | < 1.8s |
| LCP (Largest Contentful Paint) | 最大内容完成渲染 | < 2.5s |
| TBT (Total Blocking Time) | 主线程阻塞总时长 | < 200ms |
| CLS (Cumulative Layout Shift) | 累计布局偏移 | < 0.1 |
| INP (Interaction to Next Paint) | 交互延时 | < 200ms |
// 测量 Web Vitals
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log);
onINP(console.log);
onCLS(console.log);🚨 常见陷阱
- 过早优化:在确认性能问题前,memo/useMemo/useCallback 反而增加复杂度
- memo 配合内联对象/函数:传入新对象 → memo 形同虚设
- Context 大对象更新:所有消费者重渲染 → 拆分 Context 或用选择器方案
- 缺少 key:列表中没有 key 或 key 使用不稳定
- 渲染中创建新组件:每次都是"不同类型",强制卸载挂载
- 大列表不虚拟化:10000 条数据直接渲染 → 几百 ms 的白屏