React 性能优化
性能优化概述
React 性能优化的核心目标是:
- 减少不必要的渲染
- 优化渲染性能
- 减少包体积
- 提升用户体验
减少不必要的渲染
1. React.memo
作用: 对组件进行浅比较,避免不必要的重新渲染
使用:
// 基础用法
const MyComponent = React.memo(function MyComponent({ name }) {
return <div>{name}</div>;
});
// 自定义比较函数
const MyComponent = React.memo(
function MyComponent({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// 返回 true 表示 props 相同,不重新渲染
return prevProps.user.id === nextProps.user.id;
}
);
适用场景:
- 组件 props 变化不频繁
- 组件渲染成本较高
- 父组件频繁重新渲染
注意:
- 只做浅比较
- 不要过度使用,比较本身也有成本
2. useMemo
作用: 缓存计算结果,避免重复计算
使用:
function ExpensiveComponent({ items, filter }) {
// ❌ 不好:每次渲染都重新计算
const filteredItems = items.filter(item => item.type === filter);
// ✅ 好:使用 useMemo 缓存
const filteredItems = useMemo(() => {
return items.filter(item => item.type === filter);
}, [items, filter]);
return <div>{filteredItems.map(item => <Item key={item.id} item={item} />)}</div>;
}
适用场景:
- 昂贵的计算
- 依赖项变化不频繁
- 计算结果用于子组件 props
注意:
- 不要过度使用
- 比较依赖项也有成本
3. useCallback
作用: 缓存函数引用,避免子组件不必要的重新渲染
使用:
function Parent({ items }) {
const [count, setCount] = useState(0);
// ❌ 不好:每次渲染都创建新函数
const handleClick = (id) => {
console.log('clicked', id);
};
// ✅ 好:使用 useCallback 缓存函数
const handleClick = useCallback((id) => {
console.log('clicked', id);
}, []); // 依赖项为空,函数引用不变
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{items.map(item => (
<Child key={item.id} item={item} onClick={handleClick} />
))}
</div>
);
}
const Child = React.memo(({ item, onClick }) => {
return <div onClick={() => onClick(item.id)}>{item.name}</div>;
});
适用场景:
- 函数作为 props 传递给子组件
- 子组件使用 React.memo 优化
- 函数依赖项变化不频繁
注意:
- 不要过度使用
- 确保依赖项正确
4. 避免在 JSX 中创建对象和函数
问题代码:
function Component({ items }) {
return (
<div>
{/* ❌ 每次渲染都创建新对象 */}
<Child style={{ color: 'red' }} />
{/* ❌ 每次渲染都创建新函数 */}
{items.map(item => (
<Item key={item.id} onClick={() => handleClick(item.id)} />
))}
</div>
);
}
优化:
function Component({ items }) {
// ✅ 提取到组件外部或使用 useMemo
const style = useMemo(() => ({ color: 'red' }), []);
// ✅ 使用 useCallback
const handleItemClick = useCallback((id) => {
handleClick(id);
}, []);
return (
<div>
<Child style={style} />
{items.map(item => (
<Item key={item.id} onClick={handleItemClick} itemId={item.id} />
))}
</div>
);
}
列表优化
1. 正确使用 key
问题:
// ❌ 不好:使用 index 作为 key
{items.map((item, index) => (
<Item key={index} item={item} />
))}
优化:
// ✅ 好:使用唯一 ID
{items.map(item => (
<Item key={item.id} item={item} />
))}
原因:
- index 作为 key 会导致组件状态混乱
- 列表顺序变化时性能问题
- 可能导致不必要的重新渲染
2. 虚拟滚动
适用场景: 长列表渲染
实现:
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</FixedSizeList>
);
}
优势:
- 只渲染可见区域
- 减少 DOM 节点
- 提升滚动性能
3. 分页和懒加载
分页:
function PaginatedList({ items }) {
const [page, setPage] = useState(1);
const pageSize = 20;
const paginatedItems = useMemo(() => {
const start = (page - 1) * pageSize;
return items.slice(start, start + pageSize);
}, [items, page]);
return (
<div>
{paginatedItems.map(item => (
<Item key={item.id} item={item} />
))}
<button onClick={() => setPage(page + 1)}>Next</button>
</div>
);
}
懒加载:
import { useState, useEffect, useRef } from 'react';
function LazyList({ items }) {
const [visibleItems, setVisibleItems] = useState(items.slice(0, 20));
const observerRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setVisibleItems(prev => {
const nextIndex = prev.length;
return [...prev, ...items.slice(nextIndex, nextIndex + 20)];
});
}
});
observerRef.current = observer;
return () => observer.disconnect();
}, [items]);
return (
<div>
{visibleItems.map(item => (
<Item key={item.id} item={item} />
))}
<div ref={observerRef} />
</div>
);
}
代码分割
1. 路由懒加载
使用 React.lazy:
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
2. 组件懒加载
按需加载组件:
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
const [showHeavy, setShowHeavy] = useState(false);
return (
<div>
<button onClick={() => setShowHeavy(true)}>Load Heavy</button>
{showHeavy && (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
)}
</div>
);
}
3. 动态导入
使用 import():
function loadModule() {
return import('./module').then(module => {
module.doSomething();
});
}
状态管理优化
1. 状态提升和降级
状态提升:
// ❌ 不好:状态在子组件
function Parent() {
return <Child />;
}
function Child() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
// ✅ 好:状态提升到父组件
function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} />;
}
状态降级:
// ❌ 不好:状态在父组件,导致所有子组件重新渲染
function Parent() {
const [count, setCount] = useState(0);
return (
<div>
<ExpensiveChild1 />
<ExpensiveChild2 />
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}
// ✅ 好:状态降级到子组件
function Parent() {
return (
<div>
<ExpensiveChild1 />
<ExpensiveChild2 />
<Counter />
</div>
);
}
2. Context 优化
问题: Context 值变化会导致所有消费者重新渲染
优化:
// ❌ 不好:值对象每次都是新的
function App() {
const [user, setUser] = useState({ name: 'John' });
return (
<UserContext.Provider value={{ user, setUser }}>
<Child />
</UserContext.Provider>
);
}
// ✅ 好:分离 Context 或使用 useMemo
function App() {
const [user, setUser] = useState({ name: 'John' });
const value = useMemo(() => ({ user, setUser }), [user]);
return (
<UserContext.Provider value={value}>
<Child />
</UserContext.Provider>
);
}
// ✅ 更好:分离 Context
const UserContext = createContext();
const SetUserContext = createContext();
function App() {
const [user, setUser] = useState({ name: 'John' });
return (
<UserContext.Provider value={user}>
<SetUserContext.Provider value={setUser}>
<Child />
</SetUserContext.Provider>
</UserContext.Provider>
);
}
其他优化技巧
1. 避免内联样式对象
// ❌ 不好
<div style={{ color: 'red' }} />
// ✅ 好:使用 CSS 类
<div className="red-text" />
// ✅ 或提取样式对象
const style = { color: 'red' };
<div style={style} />
2. 使用生产版本
# 开发版本包含警告和调试信息,体积大
# 生产版本已优化,体积小
# 构建生产版本
npm run build
3. 使用 React DevTools Profiler
分析组件渲染:
- 打开 React DevTools
- 切换到 Profiler 标签
- 点击录制
- 执行操作
- 停止录制
- 分析性能瓶颈
4. 避免在 render 中执行副作用
// ❌ 不好:在 render 中执行副作用
function Component() {
fetchData(); // 每次渲染都执行
return <div>...</div>;
}
// ✅ 好:使用 useEffect
function Component() {
useEffect(() => {
fetchData();
}, []);
return <div>...</div>;
}
5. 使用 Web Workers 处理耗时任务
function Component() {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker('worker.js');
worker.postMessage({ data: largeData });
worker.onmessage = (e) => {
setResult(e.data);
};
return () => worker.terminate();
}, []);
return <div>{result}</div>;
}
性能监控
1. 使用 React Profiler API
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log('Component:', id);
console.log('Phase:', phase);
console.log('Duration:', actualDuration);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Component />
</Profiler>
);
}
2. 性能指标
关键指标:
- FCP (First Contentful Paint):首次内容绘制
- LCP (Largest Contentful Paint):最大内容绘制
- FID (First Input Delay):首次输入延迟
- TTI (Time to Interactive):可交互时间
最佳实践总结
1. 组件层面
- 使用 React.memo 优化组件
- 使用 useMemo 缓存计算结果
- 使用 useCallback 缓存函数引用
- 避免在 JSX 中创建对象和函数
2. 列表优化
- 正确使用 key(使用唯一 ID)
- 长列表使用虚拟滚动
- 分页和懒加载
3. 代码分割
- 路由懒加载
- 组件懒加载
- 动态导入
4. 状态管理
- 合理提升和降级状态
- Context 优化(分离或使用 useMemo)
5. 工具使用
- React DevTools Profiler
- React Profiler API
- 性能监控指标
6. 注意事项
- 不要过度优化
- 先测量再优化
- 关注用户体验
- 平衡性能和可维护性