React 性能优化
本章将介绍 React 应用中常见的性能问题及其优化方法。性能优化的核心目标是:减少不必要的渲染次数、减小渲染压力、缓存计算结果。
性能优化思路
在开始优化之前,首先要明确性能优化的三个核心思路:
- 减少渲染次数:避免不必要的组件更新
- 减小渲染压力:将耗时任务拆分或延迟
- 缓存计算结果:避免重复执行昂贵的计算逻辑
优化应该在发现性能问题后进行,而不是过早优化。使用 React DevTools Profiler 来识别真正的性能瓶颈。
1. 避免不必要的重新渲染
React.memo
React.memo 是一个高阶组件,它会对组件的 props 进行浅比较。如果 props 没有变化,React 将跳过渲染该组件。
const Child = React.memo(({ name }) => { console.log("子组件渲染了"); return ( <div style={{ padding: '15px', border: '2px solid #3b82f6', borderRadius: '8px', background: '#eff6ff' }}> <strong>子组件:</strong>你好, {name} </div> ); }); function MemoDemo() { const [count, setCount] = useState(0); const [name, setName] = useState("张三"); return ( <div style={{ padding: '15px', background: '#f8fafc', borderRadius: '8px' }}> <p style={{ fontSize: '18px' }}>父组件计数: <strong>{count}</strong></p> <div style={{ display: 'flex', gap: '10px', marginBottom: '15px' }}> <button onClick={() => setCount(c => c + 1)} style={{ padding: '8px 16px', background: '#6b7280', color: 'white', border: 'none', borderRadius: '4px' }} > 增加计数(不触发子组件重绘) </button> <button onClick={() => setName(n => n === "张三" ? "李四" : "张三")} style={{ padding: '8px 16px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px' }} > 修改名称(触发子组件重绘) </button> </div> <Child name={name} /> <p style={{ fontSize: '12px', color: '#64748b', marginTop: '10px' }}> 打开控制台查看渲染日志 </p> </div> ); }
何时使用 React.memo
不是所有组件都需要 React.memo,它本身也有开销。以下情况适合使用:
- 组件渲染开销较大(复杂的 JSX 或计算)
- 经常以相同的 props 重新渲染
- 作为列表项渲染大量相同组件
自定义比较函数
当浅比较不足以判断 props 是否变化时,可以提供自定义比较函数:
const arePropsEqual = (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
};
const UserCard = React.memo(function UserCard({ user }) {
return <div>{user.name}</div>;
}, arePropsEqual);
2. 缓存计算与回调
useMemo:缓存计算结果
useMemo 用于缓存计算结果,避免每次渲染都重新计算。
function UseMemoDemo() { const [count, setCount] = useState(0); const [text, setText] = useState(""); // 模拟昂贵的计算 const expensiveValue = useMemo(() => { console.log("执行昂贵计算..."); let result = 0; for (let i = 0; i < 1000000; i++) { result += i; } return result + count; }, [count]); // 仅在 count 变化时重新计算 return ( <div style={{ padding: '15px', background: '#f0fdf4', borderRadius: '8px' }}> <h4 style={{ margin: '0 0 12px 0', color: '#166534' }}>useMemo 演示</h4> <p style={{ fontSize: '24px', fontWeight: 'bold', color: '#15803d' }}> 计算结果: {expensiveValue.toLocaleString()} </p> <div style={{ display: 'flex', gap: '10px', alignItems: 'center', marginTop: '12px' }}> <button onClick={() => setCount(c => c + 1)} style={{ padding: '8px 16px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '4px' }} > 更新计数(触发重新计算) </button> <input value={text} onChange={(e) => setText(e.target.value)} placeholder="输入内容不触发昂贵计算" style={{ padding: '8px', border: '1px solid #d1d5db', borderRadius: '4px', flex: 1 }} /> </div> <p style={{ fontSize: '12px', color: '#64748b', marginTop: '8px' }}> 打开控制台查看计算日志 </p> </div> ); }
useCallback:缓存函数引用
useCallback 用于缓存函数定义,避免每次渲染创建新函数。这在与 React.memo 配合使用时特别重要。
const Button = React.memo(({ onClick, label }) => { console.log('Button 渲染:', label); return ( <button onClick={onClick} style={{ padding: '8px 16px', margin: '4px' }} > {label} </button> ); }); function UseCallbackDemo() { const [count, setCount] = useState(0); const [items, setItems] = useState([]); // 不使用 useCallback,每次渲染都会创建新函数 // 使用 useCallback 缓存函数引用 const handleClick = useCallback(() => { setCount(c => c + 1); }, []); const handleAddItem = useCallback(() => { setItems(prev => [...prev, `项目 ${prev.length + 1}`]); }, []); return ( <div style={{ padding: '15px', background: '#fef3c7', borderRadius: '8px' }}> <h4 style={{ margin: '0 0 12px 0' }}>useCallback 演示</h4> <p>计数: {count}</p> <div style={{ marginBottom: '12px' }}> <Button onClick={handleClick} label="增加计数" /> <Button onClick={handleAddItem} label="添加项目" /> </div> <ul style={{ margin: 0, padding: 0, listStyle: 'none' }}> {items.map((item, i) => ( <li key={i} style={{ padding: '4px', background: '#fef9c3', marginBottom: '4px', borderRadius: '4px' }}> {item} </li> ))} </ul> </div> ); }
useMemo vs useCallback
useMemo:缓存任何值的计算结果useCallback:专门缓存函数,等价于useMemo(() => fn, deps)
// 这两种写法等价
const memoizedCallback = useCallback(() => doSomething(a, b), [a, b]);
const memoizedCallback = useMemo(() => () => doSomething(a, b), [a, b]);
3. 列表优化
使用虚拟列表
当渲染包含数千个条目的列表时,只渲染可见区域的 DOM 节点可以大幅提升性能。推荐使用 react-window 或 @tanstack/react-virtual。
Key 的正确使用
正确使用 key 可以让 React 更高效地更新列表:
function KeyDemo() { const [items, setItems] = useState([ { id: 'a1', text: '第一条项目' }, { id: 'b2', text: '第二条项目' }, { id: 'c3', text: '第三条项目' } ]); const addItem = () => { const newId = String(Date.now()); setItems(prev => [{ id: newId, text: `新项目 ${newId}` }, ...prev]); }; const reverseItems = () => { setItems(prev => [...prev].reverse()); }; return ( <div style={{ padding: '15px' }}> <div style={{ marginBottom: '12px', display: 'flex', gap: '8px' }}> <button onClick={addItem} style={{ padding: '6px 12px' }}>添加项目</button> <button onClick={reverseItems} style={{ padding: '6px 12px' }}>反转列表</button> </div> <ul style={{ listStyle: 'none', padding: 0 }}> {items.map(item => ( <li key={item.id} style={{ padding: '10px', background: '#f8fafc', marginBottom: '4px', borderRadius: '4px', borderLeft: '4px solid #3b82f6' }} > <code style={{ color: '#6b7280' }}>key: {item.id}</code> <span style={{ marginLeft: '10px' }}>{item.text}</span> </li> ))} </ul> </div> ); }
Key 使用规则:
- 使用稳定的、唯一的 ID,不要用数组索引
- 不要在渲染时动态生成 key(如
Math.random()) - Key 只需要在兄弟节点中唯一
4. 并发渲染(React 18+)
React 18 引入了并发特性,允许将更新标记为不同的优先级。
useTransition
useTransition 用于将紧急更新(如输入文字)与非紧急更新(如过滤列表)分离,保持 UI 响应。
function TransitionDemo() { const [query, setQuery] = useState(""); const [list, setList] = useState([]); const [isPending, startTransition] = useTransition(); const handleChange = (e) => { const value = e.target.value; setQuery(value); // 紧急更新:立即反映输入 // 非紧急更新:在后台处理列表过滤 startTransition(() => { const items = Array.from({ length: 5000 }, (_, i) => `${value} 项目 ${i + 1}`); setList(items); }); }; return ( <div style={{ padding: '15px', background: '#faf5ff', borderRadius: '8px' }}> <h4 style={{ margin: '0 0 12px 0', color: '#7c3aed' }}>useTransition 演示</h4> <input value={query} onChange={handleChange} placeholder="键入文字查看并发渲染..." style={{ width: '100%', padding: '10px', border: '1px solid #d1d5db', borderRadius: '4px', marginBottom: '12px', fontSize: '16px' }} /> {isPending && ( <p style={{ color: '#7c3aed', fontStyle: 'italic' }}>正在计算过滤结果...</p> )} <ul style={{ maxHeight: '150px', overflow: 'auto', listStyle: 'none', padding: 0, background: 'white', borderRadius: '4px', border: '1px solid #e2e8f0' }}> {list.slice(0, 50).map((item, i) => ( <li key={i} style={{ padding: '6px 10px', borderBottom: '1px solid #f1f5f9' }}> {item} </li> ))} {list.length > 50 && ( <li style={{ padding: '6px 10px', color: '#64748b', textAlign: 'center' }}> ... 还有 {list.length - 50} 条 </li> )} </ul> </div> ); }
useDeferredValue
useDeferredValue 让你可以延迟更新某个值,适用于无法控制更新源的场景:
function SearchResults({ query }) {
// 延迟查询值,让输入保持响应
const deferredQuery = useDeferredValue(query);
return (
<div>
{deferredQuery && <ResultList query={deferredQuery} />}
</div>
);
}
5. 代码分割
React.lazy 和 Suspense
通过动态导入组件,减少首屏加载的包体积:
import { lazy, Suspense } from 'react';
// 懒加载组件
const HeavyComponent = lazy(() => import('./HeavyComponent'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>
);
}
路由级代码分割
最常见的代码分割场景是与路由配合:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
预加载
可以在用户可能需要的组件之前预加载:
// 鼠标悬停时预加载
const handleMouseEnter = () => {
import('./HeavyComponent');
};
<button onMouseEnter={handleMouseEnter}>
加载重型组件
</button>
6. 其他优化技巧
防抖和节流
对于高频触发的事件,使用防抖或节流:
function DebounceDemo() { const [searchTerm, setSearchTerm] = useState(''); const [debouncedTerm, setDebouncedTerm] = useState(''); const [results, setResults] = useState([]); // 防抖函数 useEffect(() => { const timer = setTimeout(() => { setDebouncedTerm(searchTerm); }, 500); return () => clearTimeout(timer); }, [searchTerm]); // 模拟搜索 useEffect(() => { if (debouncedTerm) { setResults([ `${debouncedTerm} - 结果 1`, `${debouncedTerm} - 结果 2`, `${debouncedTerm} - 结果 3` ]); } }, [debouncedTerm]); return ( <div style={{ padding: '15px', background: '#ecfdf5', borderRadius: '8px' }}> <h4 style={{ margin: '0 0 12px 0', color: '#059669' }}>防抖演示</h4> <input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} placeholder="输入搜索内容(500ms 防抖)" style={{ width: '100%', padding: '10px', border: '1px solid #d1d5db', borderRadius: '4px' }} /> <p style={{ fontSize: '14px', color: '#6b7280' }}> 输入值: {searchTerm} | 防抖值: {debouncedTerm} </p> {results.length > 0 && ( <ul style={{ margin: 0, padding: '0 0 0 20px' }}> {results.map((r, i) => <li key={i}>{r}</li>)} </ul> )} </div> ); }
避免内联对象和函数
内联对象和函数在每次渲染时都会创建新引用,可能导致子组件不必要的重渲染:
// 避免:每次渲染都创建新对象
<Button style={{ color: 'red' }} onClick={() => doSomething()} />
// 推荐:提取到组件外部或使用 useMemo/useCallback
const buttonStyle = { color: 'red' };
const handleClick = useCallback(() => doSomething(), []);
<Button style={buttonStyle} onClick={handleClick} />
使用 Fragment 减少嵌套
减少不必要的 DOM 节点:
// 不好的做法:添加不必要的 div
function List() {
return (
<div>
<h1>标题</h1>
<ul>{items}</ul>
</div>
);
}
// 好的做法:使用 Fragment
function List() {
return (
<>
<h1>标题</h1>
<ul>{items}</ul>
</>
);
}
常见性能问题及解决方案
| 问题 | 症状 | 解决方案 |
|---|---|---|
| 不必要的重新渲染 | 组件频繁渲染 | React.memo、useMemo、useCallback |
| 大列表渲染慢 | 滚动卡顿 | 虚拟列表、分页、懒加载 |
| 重复计算 | CPU 占用高 | useMemo |
| 重复创建函数 | 子组件重渲染 | useCallback |
| 大 Bundle | 首屏加载慢 | 代码分割、懒加载 |
| 输入延迟 | 打字卡顿 | useTransition、防抖 |
| 内存泄漏 | 内存持续增长 | 清理 useEffect、取消订阅 |
性能检查清单
优化前,使用 React DevTools Profiler 识别真正的瓶颈。以下是一个检查清单:
渲染优化
- 使用 React DevTools Profiler 分析
- 检查不必要的重新渲染
- 使用 React.memo 包装纯组件
- 使用 useMemo 缓存计算结果
- 使用 useCallback 缓存回调函数
列表优化
- 使用正确的 key
- 考虑虚拟列表
- 实现分页或懒加载
加载优化
- 实现代码分割
- 使用 React.lazy 懒加载
- 预加载关键资源
其他
- 清理副作用和取消订阅
- 生产环境构建
- 监控性能指标
小结
本章我们学习了 React 性能优化的核心方法:
避免不必要的渲染
React.memo:跳过 props 未变化的组件渲染useMemo:缓存昂贵的计算结果useCallback:缓存函数引用
列表优化
- 使用稳定的 key
- 虚拟列表处理大数据
并发渲染
useTransition:分离紧急和非紧急更新useDeferredValue:延迟更新某些值
代码分割
React.lazy:懒加载组件- 配合路由实现页面级分割
最佳实践
- 使用 React DevTools Profiler 识别瓶颈
- 避免过早优化
- 在正确的地方使用正确的优化技术
练习
- 创建一个列表组件,渲染 1000 条数据,比较使用
React.memo前后的性能差异 - 实现一个搜索组件,使用
useTransition优化大列表过滤 - 使用
useMemo优化一个复杂的数据转换组件 - 实现一个带防抖的搜索输入框
- 使用 React DevTools Profiler 分析你的应用,找出性能瓶颈