React 性能优化策略
PureComponent、React.memo、useMemo、useCallback 的使用场景和实践
问题
React 应用中如何避免不必要的重新渲染?常用的优化手段有哪些?
解答
1. PureComponent(类组件)
PureComponent 自动对 props 和 state 进行浅比较,避免不必要的渲染。
import React, { PureComponent, Component } from 'react';
// 普通组件:父组件更新时总会重新渲染
class RegularChild extends Component {
render() {
console.log('RegularChild 渲染');
return <div>{this.props.name}</div>;
}
}
// PureComponent:props 浅比较相等时跳过渲染
class PureChild extends PureComponent {
render() {
console.log('PureChild 渲染');
return <div>{this.props.name}</div>;
}
}
class Parent extends Component {
state = { count: 0, name: 'Tom' };
render() {
return (
<div>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Count: {this.state.count}
</button>
{/* 点击按钮时,RegularChild 会重新渲染,PureChild 不会 */}
<RegularChild name={this.state.name} />
<PureChild name={this.state.name} />
</div>
);
}
}
2. React.memo(函数组件)
React.memo 是函数组件版本的 PureComponent。
import React, { useState, memo } from 'react';
// 普通函数组件
const RegularChild = ({ name }) => {
console.log('RegularChild 渲染');
return <div>{name}</div>;
};
// memo 包裹:props 不变时跳过渲染
const MemoChild = memo(({ name }) => {
console.log('MemoChild 渲染');
return <div>{name}</div>;
});
// 自定义比较函数
const MemoChildCustom = memo(
({ user }) => {
console.log('MemoChildCustom 渲染');
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// 返回 true 表示 props 相等,跳过渲染
return prevProps.user.id === nextProps.user.id;
}
);
function Parent() {
const [count, setCount] = useState(0);
const name = 'Tom';
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<RegularChild name={name} />
<MemoChild name={name} />
</div>
);
}
3. useMemo(缓存计算结果)
useMemo 缓存计算结果,依赖不变时返回缓存值。
import React, { useState, useMemo } from 'react';
function ExpensiveList({ items, filter }) {
// 没有 useMemo:每次渲染都重新计算
// const filteredItems = items.filter(item => item.includes(filter));
// 使用 useMemo:只在 items 或 filter 变化时重新计算
const filteredItems = useMemo(() => {
console.log('执行过滤计算');
return items.filter(item => item.includes(filter));
}, [items, filter]);
return (
<ul>
{filteredItems.map(item => (
<li key={item}>{item}</li>
))}
</ul>
);
}
function App() {
const [filter, setFilter] = useState('');
const [count, setCount] = useState(0);
const items = ['apple', 'banana', 'orange', 'grape'];
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
{/* 点击按钮不会触发过滤计算 */}
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveList items={items} filter={filter} />
</div>
);
}
4. useCallback(缓存函数引用)
useCallback 缓存函数引用,配合 memo 使用避免子组件重新渲染。
import React, { useState, useCallback, memo } from 'react';
// memo 包裹的子组件
const Button = memo(({ onClick, children }) => {
console.log(`Button "${children}" 渲染`);
return <button onClick={onClick}>{children}</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 没有 useCallback:每次渲染创建新函数,Button 总会重新渲染
// const handleClick = () => setCount(count + 1);
// 使用 useCallback:函数引用稳定,Button 不会因为 text 变化而重新渲染
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<p>Count: {count}</p>
<Button onClick={handleClick}>增加</Button>
</div>
);
}
5. 避免内联函数和对象
内联函数和对象每次渲染都会创建新引用,导致子组件重新渲染。
import React, { useState, memo, useCallback, useMemo } from 'react';
const Child = memo(({ style, onClick }) => {
console.log('Child 渲染');
return <div style={style} onClick={onClick}>Child</div>;
});
// ❌ 错误示范
function BadParent() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
{/* 每次渲染都创建新的 style 对象和 onClick 函数 */}
<Child
style={{ color: 'red' }}
onClick={() => console.log('clicked')}
/>
</div>
);
}
// ✅ 正确示范
function GoodParent() {
const [count, setCount] = useState(0);
// 缓存对象
const style = useMemo(() => ({ color: 'red' }), []);
// 缓存函数
const handleClick = useCallback(() => console.log('clicked'), []);
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<Child style={style} onClick={handleClick} />
</div>
);
}
完整示例
import React, { useState, useCallback, useMemo, memo } from 'react';
// 列表项组件
const ListItem = memo(({ item, onRemove }) => {
console.log(`ListItem ${item.id} 渲染`);
return (
<li>
{item.text}
<button onClick={() => onRemove(item.id)}>删除</button>
</li>
);
});
// 列表组件
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React' },
{ id: 2, text: '写代码' },
]);
const [input, setInput] = useState('');
const [filter, setFilter] = useState('');
// 缓存删除函数
const handleRemove = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
// 缓存添加函数
const handleAdd = useCallback(() => {
if (!input.trim()) return;
setTodos(prev => [...prev, { id: Date.now(), text: input }]);
setInput('');
}, [input]);
// 缓存过滤结果
const filteredTodos = useMemo(() => {
return todos.filter(todo => todo.text.includes(filter));
}, [todos, filter]);
return (
<div>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="添加待办"
/>
<button onClick={handleAdd}>添加</button>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="搜索"
/>
<ul>
{filteredTodos.map(item => (
<ListItem key={item.id} item={item} onRemove={handleRemove} />
))}
</ul>
</div>
);
}
关键点
- PureComponent 用于类组件,自动浅比较 props 和 state
- React.memo 用于函数组件,可传入自定义比较函数
- useMemo 缓存计算结果,避免重复计算
- useCallback 缓存函数引用,配合 memo 使用才有意义
- 避免内联 函数和对象作为 props,否则每次渲染都是新引用
- 不要过度优化,先用 React DevTools Profiler 定位性能瓶颈
目录