Hook
React 18+
useMemo & useCallback:性能优化
学习如何使用 useMemo 和 useCallback 优化 React 组件性能
为什么要性能优化?
React 默认会在组件的状态或 Props 改变时重新渲染组件。在某些情况下,这可能导致不必要的计算或渲染。
常见性能问题
- 昂贵的计算在每次渲染时都重新执行
- 子组件因为父组件重新渲染而不必要地重新渲染
- 函数引用在每次渲染时都改变,导致子组件失去优化
重要:优化原则
性能优化的三个原则:
- 先测量,后优化:使用 React DevTools Profiler 识别真正的性能瓶颈
- 避免过早优化:简单计算(如加减乘除、字符串拼接)不需要 useMemo
- 优化有成本:useMemo 和 useCallback 本身也有开销,只在确实需要时使用
判断标准:如果一个计算的执行时间超过 1ms,或者组件每秒渲染超过 60 次, 才考虑使用 useMemo 优化。
何时不需要优化
TypeScript
// ✅ 这些情况不需要 useMemo
function Component({ a, b }) {
// 简单数学运算(< 0.001ms)
const sum = a + b;
// 简单字符串操作(< 0.001ms)
const greeting = `Hello, ${name}`;
// 简单数组映射(小数组,< 1ms)
const doubled = numbers.map(n => n * 2);
return <div>{sum}</div>;
}
// ❌ 不要这样优化(开销大于收益)
function Component({ a, b }) {
const sum = useMemo(() => a + b, [a, b]); // 不必要!
return <div>{sum}</div>;
}
useMemo:缓存计算结果
useMemo 缓存计算结果,只有在依赖项改变时才重新计算。
这就好比你把计算结果记在笔记本上,下次如果输入没变,直接查笔记本就行,不用重新算一遍。
基本语法
TypeScript
import { useMemo } from 'react';
const cachedValue = useMemo(calculateValue, dependencies);
- calculateValue: 计算缓存值的函数
- dependencies: 依赖数组,只有这些值改变时才重新计算
示例:耗费性能的过滤操作
TypeScript
import { useMemo, useState } from 'react';
function ProductList({ products }) {
const [filter, setFilter] = useState('');
// ❌ 不使用 useMemo:每次渲染都重新过滤
// const filteredProducts = products.filter(p =>
// p.name.toLowerCase().includes(filter.toLowerCase())
// );
// ✅ 使用 useMemo:只在 products 或 filter 改变时重新计算
const filteredProducts = useMemo(() => {
console.log('过滤产品...');
return products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
);
}, [products, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="搜索产品..."
/>
<ul>
{filteredProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
示例:复杂计算
TypeScript
function FibonacciCalculator({ n }) {
// ✅ 缓存斐波那契数列计算结果
const result = useMemo(() => {
console.log(`计算斐波那契数列第 ${n} 项`);
function fib(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
return fib(n);
}, [n]);
return <div>斐波那契数列第 {n} 项: {result}</div>;
}
何时使用 useMemo?
- 计算成本高(如复杂的数据转换)
- 依赖项很少改变
- 计算结果用于渲染或其他 Hook
useCallback:缓存函数引用
useCallback 缓存函数定义,只有在依赖项改变时才返回新的函数引用。
基本语法
TypeScript
import { useCallback } from 'react';
const cachedFn = useCallback(fn, dependencies);
- fn: 要缓存的函数
- dependencies: 依赖数组
useCallback vs useMemo
TypeScript
// useCallback 实际上是 useMemo 的语法糖
// 使用 useCallback
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// 等价于使用 useMemo
const memoizedCallback = useMemo(() => {
return () => doSomething(a, b);
}, [a, b]);
示例:传递给优化的子组件
TypeScript
import { useCallback, memo, useState } from 'react';
// 子组件使用 memo 优化
const Button = memo(function Button({ onClick, children }) {
console.log('Button 渲染');
return <button onClick={onClick}>{children}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ❌ 不使用 useCallback:每次渲染都创建新函数
// const handleClick = () => {
// console.log('点击了!');
// };
// ✅ 使用 useCallback:函数引用稳定
const handleClick = useCallback(() => {
console.log('点击了!');
}, []); // 空依赖数组,函数永不改变
return (
<div>
<Button onClick={handleClick}>点击我</Button>
<button onClick={() => setCount(count + 1)}>
计数: {count}
</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
为什么需要稳定的函数引用?
如果子组件使用 React.memo 优化,但父组件每次渲染都传递新的函数引用,子组件还是会重新渲染。使用 useCallback 可以保持函数引用稳定。
示例:带依赖的 useCallback
TypeScript
function ProductList({ products }) {
const [cart, setCart] = useState([]);
// ✅ 只在 products 改变时重新创建函数
const addToCart = useCallback((productId) => {
const product = products.find(p => p.id === productId);
setCart(prev => [...prev, product]);
}, [products]);
return (
<div>
{products.map(product => (
<ProductItem
key={product.id}
product={product}
onAddToCart={addToCart}
/>
))}
</div>
);
}
const ProductItem = memo(function ProductItem({ product, onAddToCart }) {
return (
<div>
<span>{product.name}</span>
<button onClick={() => onAddToCart(product.id)}>
添加到购物车
</button>
</div>
);
});
React.memo:跳过不必要的渲染
React.memo 是一个高阶组件,如果 Props 没有改变,就跳过组件的渲染。
基本用法
TypeScript
import { memo } from 'react';
const MyComponent = memo(function MyComponent(props) {
// 只有在 Props 改变时才重新渲染
return <div>{props.name}</div>;
});
示例:优化列表项
TypeScript
import { memo } from 'react';
// 使用 memo 优化列表项组件
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
console.log(`TodoItem ${todo.id} 渲染`);
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
);
});
function TodoList() {
const [todos, setTodos] = useState([
{ id: 1, text: '学习 React', completed: false },
{ id: 2, text: '学习 TypeScript', completed: false },
]);
// 使用 useCallback 稳定函数引用
const handleToggle = useCallback((id) => {
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
</ul>
);
}
React.memo 的浅比较
React.memo 默认使用浅比较来检查 Props。如果需要自定义比较逻辑,可以传递比较函数作为第二个参数。
TypeScript
const MyComponent = memo(
function MyComponent(props) {
return <div>{props.name}</div>;
},
(prevProps, nextProps) => {
// 返回 true 表示 Props 相等,不需要重新渲染
return prevProps.name === nextProps.name;
}
);
使用场景
场景 1:昂贵的计算
TypeScript
function DataTable({ data, sortBy }) {
// ✅ 缓存排序后的数据
const sortedData = useMemo(() => {
return data.sort((a, b) => {
if (a[sortBy] < b[sortBy]) return -1;
if (a[sortBy] > b[sortBy]) return 1;
return 0;
});
}, [data, sortBy]);
return <table>...</table>;
}
场景 2:防止子组件重新渲染
TypeScript
function Parent() {
const [count, setCount] = useState(0);
// ✅ 稳定的函数引用
const handleClick = useCallback(() => {
console.log('点击');
}, []);
return (
<>
<button onClick={() => setCount(count + 1)}>
计数: {count}
</button>
<ExpensiveChild onClick={handleClick} />
</>
);
}
const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
console.log('ExpensiveChild 渲染');
return <button onClick={onClick}>子组件</button>;
});
场景 3:作为其他 Hook 的依赖
TypeScript
function Chat({ roomId }) {
const [message, setMessage] = useState('');
// ✅ 使用 useCallback 确保 messageHandler 引用稳定
const messageHandler = useCallback((msg) => {
console.log('收到消息:', msg);
}, []);
useEffect(() => {
const connection = connectToChat(roomId, messageHandler);
return () => connection.disconnect();
}, [roomId, messageHandler]);
return <input value={message} onChange={(e) => setMessage(e.target.value)} />;
}
常见错误
1. 过度优化
TypeScript
// ❌ 差:简单计算不需要 useMemo
function Component({ a, b }) {
const result = useMemo(() => a + b, [a, b]);
return <div>{result}</div>;
}
// ✅ 好:直接计算
function Component({ a, b }) {
return <div>{a + b}</div>;
}
2. 忘记依赖
TypeScript
// ❌ 错误:缺少依赖
const filtered = useCallback(() => {
return items.filter(item => item.active);
}, []); // 应该包含 items
// ✅ 正确:包含所有依赖
const filtered = useCallback(() => {
return items.filter(item => item.active);
}, [items]);
3. 在 useMemo 中执行副作用
TypeScript
// ❌ 错误:在 useMemo 中执行副作用
const value = useMemo(() => {
localStorage.setItem('key', JSON.stringify(data)); // 副作用!
return data;
}, [data]);
// ✅ 正确:在 useEffect 中执行副作用
useEffect(() => {
localStorage.setItem('key', JSON.stringify(data));
}, [data]);
4. 误认为 useCallback 总是提升性能
TypeScript
// ❌ 差:没有必要的 useCallback
function Form() {
const [value, setValue] = useState('');
const handleChange = useCallback((e) => {
setValue(e.target.value);
}, []); // 没有意义,因为没有子组件使用这个函数
return <input value={value} onChange={handleChange} />;
}
// ✅ 好:直接定义函数
function Form() {
const [value, setValue] = useState('');
const handleChange = (e) => {
setValue(e.target.value);
};
return <input value={value} onChange={handleChange} />;
}
性能测量
在优化之前,应该先测量性能以确定真正的瓶颈。
使用 React DevTools Profiler
TypeScript
import { Profiler } from 'react';
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions
) {
console.log({
id,
phase,
actualDuration,
baseDuration,
});
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Navigation />
<Main />
</Profiler>
);
}
使用 performance.mark
TypeScript
function ExpensiveComponent({ data }) {
performance.mark('expensive-start');
const result = useMemo(() => {
// 耗时计算
return processData(data);
}, [data]);
performance.mark('expensive-end');
performance.measure(
'expensive-calculation',
'expensive-start',
'expensive-end'
);
return <div>{result}</div>;
}
最佳实践
- 先测量,再优化
使用 React DevTools Profiler 确定真正的性能瓶颈
- 只优化必要的部分
不要对所有组件和函数都使用 useMemo/useCallback
- 保持依赖数组准确
依赖数组必须包含所有外部值,否则可能产生过期的闭包
- 避免在 useMemo 中执行副作用
副作用应该在 useEffect 中执行,而不是在 useMemo 中
- 考虑使用 useReducer 减少依赖
如果依赖数组很长,考虑使用 useReducer 重构
TypeScript
// ❌ 很多依赖
const handleSomething = useCallback(() => {
doSomething(a, b, c, d, e, f);
}, [a, b, c, d, e, f]);
// ✅ 使用 useReducer 减少
const [state, dispatch] = useReducer(reducer, initialState);
const handleSomething = useCallback(() => {
dispatch({ type: 'DO_SOMETHING' });
}, []); // 无依赖!
下一步
现在你已经了解了性能优化,可以继续学习:
这篇文章有帮助吗?
Previous / Next
Related Links