学习路径
高级
性能优化最佳实践
学习如何优化 React 应用的性能,提升用户体验
概述
React 应用性能优化的核心思想是:减少不必要的渲染和计算。通过合理使用 React 提供的工具和最佳实践,可以让应用运行得更快、更流畅。
性能优化的重要性
- 更好的用户体验:页面响应更快,交互更流畅
- 更低的资源消耗:减少 CPU 和内存使用
- 更好的移动端体验:电池寿命更长,流量消耗更少
- 更高的 SEO 评分:页面加载速度影响搜索排名
过早优化是万恶之源
在优化之前,先确认性能瓶颈在哪里。使用 React DevTools Profiler 和浏览器性能分析工具来识别真正需要优化的地方。
1. 使用 React.memo 避免不必要的重渲染
问题场景
当父组件渲染时,所有子组件都会重新渲染,即使 props 没有变化:
TypeScript
function ExpensiveComponent({ data }: { data: UserData }) {
console.log('ExpensiveComponent 渲染');
// 复杂的计算或渲染逻辑
return <div>{data.name}</div>;
}
function Parent() {
const [count, setCount] = useState(0);
const [data] = useState({ name: 'Taylor' });
return (
<div>
<button onClick={() => setCount(count + 1)}>
点击次数: {count}
</button>
{/* count 改变时,ExpensiveComponent 也会重新渲染 */}
<ExpensiveComponent data={data} />
</div>
);
}
解决方案:React.memo
TypeScript
import { memo } from 'react';
// 使用 memo 包装组件
const ExpensiveComponent = memo(function ExpensiveComponent(
{ data }: { data: UserData }
) {
console.log('ExpensiveComponent 渲染');
return <div>{data.name}</div>;
});
// 现在只有 data 变化时才会重新渲染
自定义比较函数
TypeScript
const ExpensiveComponent = memo(
function ExpensiveComponent({ data }: { data: UserData }) {
return <div>{data.name}</div>;
},
// 自定义比较函数
(prevProps, nextProps) => {
// 返回 true 表示 props 相等,不需要重新渲染
return prevProps.data.id === nextProps.data.id;
}
);
常见错误
不要在默认比较函数中使用内联对象或数组:
// ❌ 错误:每次都是新对象
<ExpensiveComponent data={{ name: 'Taylor' }} />
// ✅ 正确:使用 useMemo 或移到外部
const data = { name: 'Taylor' };
<ExpensiveComponent data={data} />2. 使用 useMemo 缓存计算结果
问题场景
每次渲染都执行昂贵的计算:
TypeScript
function ProductList({ products }: { products: Product[] }) {
// 每次渲染都会重新计算
const sortedProducts = products.sort((a, b) =>
a.price - b.price
);
return (
<ul>
{sortedProducts.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
解决方案:useMemo
TypeScript
import { useMemo } from 'react';
function ProductList({ products }: { products: Product[] }) {
// 只在 products 变化时重新计算
const sortedProducts = useMemo(
() => products.sort((a, b) => a.price - b.price),
[products] // 依赖数组
);
return (
<ul>
{sortedProducts.map(p => (
<li key={p.id}>{p.name}</li>
))}
</ul>
);
}
适用场景
TypeScript
// ✅ 适合:昂贵的计算
const filteredData = useMemo(
() => data.filter(item => item.isActive),
[data]
);
// ✅ 适合:复杂对象
const formData = useMemo(
() => ({
username,
email,
preferences: userPreferences,
}),
[username, email, userPreferences]
);
// ❌ 不适合:简单计算
const doubled = useMemo(() => count * 2, [count]);
// 直接写:const doubled = count * 2;
3. 使用 useCallback 缓存函数
问题场景
每次渲染都创建新的函数引用,导致子组件不必要的重渲染:
TypeScript
function Parent() {
const [count, setCount] = useState(0);
// 每次渲染都是新函数
const handleClick = () => {
console.log('Clicked');
};
return <Child onClick={handleClick} />;
}
const Child = memo(function Child({
onClick
}: {
onClick: () => void;
}) {
console.log('Child 渲染');
return <button onClick={onClick}>点击</button>;
});
解决方案:useCallback
TypeScript
import { useCallback } from 'react';
function Parent() {
const [count, setCount] = useState(0);
// 函数引用保持稳定
const handleClick = useCallback(() => {
console.log('Clicked');
}, []); // 依赖数组为空,函数永不改变
return <Child onClick={handleClick} />;
}
带依赖的 useCallback
TypeScript
function ProductList({ products }: { products: Product[] }) {
const [filter, setFilter] = useState('');
// filter 变化时才创建新函数
const filteredProducts = useCallback(
() => products.filter(p => p.name.includes(filter)),
[products, filter]
);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
/>
<ProductList products={filteredProducts()} />
</div>
);
}
useCallback 和 useMemo 的关系
useCallback(fn, deps) 等价于
useMemo(() => fn, deps)。
useCallback 是专门用于缓存函数的语法糖。
4. 虚拟化长列表
问题场景
渲染大量 DOM 元素会导致性能问题:
TypeScript
// ❌ 渲染 10000 个元素
function BigList({ items }: { items: Item[] }) {
return (
<div>
{items.map(item => (
<div key={item.id}>
{item.name}
</div>
))}
</div>
);
}
解决方案:虚拟滚动
使用专业的虚拟滚动库,只渲染可见区域的元素:
TypeScript
// 使用 react-window
import { FixedSizeList } from 'react-window';
function VirtualList({ items }: { items: Item[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
1
安装虚拟滚动库:
npm install react-window2
使用
FixedSizeList(固定高度)或 VariableSizeList(可变高度)3
只渲染可见区域的项目,大幅提升性能
4
适用于超过 1000 个元素的列表
5. 代码分割和懒加载
使用 React.lazy 懒加载组件
TypeScript
import { lazy, Suspense } from 'react';
// 懒加载组件
const HeavyComponent = lazy(() =>
import('./HeavyComponent')
);
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<HeavyComponent />
</Suspense>
);
}
路由级别的代码分割
TypeScript
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'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>加载中...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Next.js 自动代码分割
TypeScript
// Next.js 会自动为每个页面进行代码分割
// pages/dashboard.tsx 会自动分割成独立的 chunk
export default function Dashboard() {
return <div>Dashboard</div>;
}
6. 避免不必要的匿名函数
问题场景
在 JSX 中创建匿名函数会导致子组件不必要的重渲染:
TypeScript
// ❌ 每次渲染都是新函数
function List() {
return (
<div>
{items.map(item => (
<button
key={item.id}
onClick={() => handleClick(item.id)} // 新函数
>
{item.name}
</button>
))}
</div>
);
}
解决方案 1:提取函数
TypeScript
// ✅ 提取函数
function List() {
const handleItemClick = (id: string) => {
handleClick(id);
};
return (
<div>
{items.map(item => (
<button
key={item.id}
onClick={() => handleItemClick(item.id)}
>
{item.name}
</button>
))}
</div>
);
}
解决方案 2:使用 data 属性
TypeScript
// ✅ 使用 data 属性
function List() {
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
const id = e.currentTarget.dataset.id;
handleClick(id);
};
return (
<div>
{items.map(item => (
<button
key={item.id}
data-id={item.id}
onClick={handleClick}
>
{item.name}
</button>
))}
</div>
);
}
7. 优化 State 结构
避免冗余 State
TypeScript
// ❌ 冗余的 state
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
const updateFirstName = (name: string) => {
setFirstName(name);
setFullName(`${name} ${lastName}`); // 需要同步更新
};
// ...
}
// ✅ 派生 state
function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// fullName 可以从 firstName 和 lastName 派生
const fullName = `${firstName} ${lastName}`;
// ...
}
合并相关 State
TypeScript
// ❌ 分散的 state
function Profile() {
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
// ...
}
// ✅ 合并为对象
function Profile() {
const [user, setUser] = useState({
name: '',
age: 0,
email: '',
});
const updateField = (field: string, value: string | number) => {
setUser(prev => ({ ...prev, [field]: value }));
};
// ...
}
何时使用 useReducer
当 state 逻辑复杂且涉及多个子值,或者下一个 state 依赖于之前的
state 时,考虑使用 useReducer。
8. 使用 key 优化列表渲染
正确使用 key
TypeScript
// ❌ 使用索引作为 key(只在静态列表时可以)
function List({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
);
}
// ✅ 使用稳定的唯一标识符
function List({ items }: { items: Item[] }) {
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
key 的作用
为什么 key 很重要?
key 帮助 React 识别哪些元素改变了、添加了或删除了。 正确的 key 可以让 React 复用 DOM 元素,避免不必要的操作。
- 使用稳定的、唯一的、可预测的值
- 不要使用索引(如果列表会重新排序)
- 不要使用随机数或时间戳
9. 使用 Transition 优先级更新
标记非紧急更新
TypeScript
import { useState, useTransition } from 'react';
function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// 紧急更新:立即更新输入框
setQuery(value);
// 非紧急更新:搜索结果可以稍后
startTransition(() => {
setSearchResults(filterResults(value));
});
};
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <div>搜索中...</div>}
<Results items={searchResults} />
</div>
);
}
Transition 使用场景
- 搜索框输入
- 过滤大型列表
- 页面导航
- 任何可以延迟的非阻塞 UI 更新
10. 避免在渲染中创建对象
问题场景
TypeScript
// ❌ 每次渲染都创建新对象
function Button() {
return (
<button
style={{ color: 'red', fontSize: '16px' }} // 新对象
>
点击
</button>
);
}
解决方案
TypeScript
// ✅ 移到组件外部
const buttonStyle = { color: 'red', fontSize: '16px' };
function Button() {
return <button style={buttonStyle}>点击</button>;
}
// 或使用 useMemo
function Button() {
const style = useMemo(
() => ({ color: 'red', fontSize: '16px' }),
[]
);
return <button style={style}>点击</button>;
}
性能检测工具
React DevTools Profiler
- 安装 React DevTools 浏览器扩展
- 打开 Profiler 标签
- 点击录制
- 与应用交互
- 停止录制并分析
使用示例
TypeScript
// 标记组件用于 Profiler
import { Profiler } from 'react';
function onRenderCallback(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) {
console.log({
id,
phase,
actualDuration,
baseDuration,
});
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Navigation />
<MainContent />
</Profiler>
);
}
检查清单
在优化性能时,按以下顺序检查:
1
使用 Profiler 识别性能瓶颈
2
避免不必要的重渲染(React.memo)
3
缓存昂贵的计算(useMemo)
4
缓存函数引用(useCallback)
5
虚拟化长列表
6
代码分割和懒加载
7
优化 state 结构
8
使用 transition 优化非紧急更新
记住
过早优化是万恶之源。只优化真正影响用户体验的 性能问题。大多数情况下,React 默认的行为已经足够好了。
相关资源
这篇文章有帮助吗?
Previous / Next
Related Links