为什么不要滥用 useMemo 和 useCallback?过度优化反而更卡
在 React 性能优化中,useMemo 和 useCallback 是最容易被误解和滥用的两个 Hook。
很多开发者一看到"性能优化"就本能地给所有函数、对象、计算结果包一层 useMemo / useCallback,结果却是:代码变复杂了,性能反而变差了。
这篇文章会讲清楚:
useMemo / useCallback到底在解决什么问题- 为什么"乱用"会导致负优化
- 什么时候该用,什么时候不该用
- 正确的性能优化心智模型
一、先搞懂:useMemo 和 useCallback 是干什么的?
useMemo:缓存"值"
ini
const computedValue = useMemo(() => {
return heavyCompute(a, b);
}, [a, b]);
✅ 只在 a 或 b 变化时,才重新计算
useCallback:缓存"函数引用"
ini
const handleClick = useCallback(() => {
console.log(a);
}, [a]);
等价于:
ini
const handleClick = useMemo(() => {
return () => console.log(a);
}, [a]);
✅ 函数引用不变,避免子组件不必要的重新渲染
二、它们解决的核心问题只有一个
防止"引用类型"频繁变化,导致子组件不必要地 re-render
问题示例(没有 useCallback)
ini
const Parent = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log('click');
};
return <Child onClick={handleClick} />;
};
每次 Parent 渲染:
handleClick都是新函数Child的props.onClick变化Child重新渲染(即使用了React.memo)
正确优化(使用 useCallback)
ini
const handleClick = useCallback(() => {
console.log('click');
}, []);
✅ 函数引用稳定
✅ Child 只有在真正需要时才渲染
三、为什么"滥用"反而更慢?
1️⃣ useMemo / useCallback 本身有成本
它们不是"免费"的,内部做了这些事情:
- 记录上一次的依赖数组
- 每次 render 进行浅比较
- 维护缓存结构
一句话:记忆化本身也是开销
2️⃣ 过早优化 = 负优化
scss
const obj = useMemo(() => ({ a: 1 }), []);
这个对象创建成本极低,但:
- React 每次 render 都要比较依赖数组
- 还要维护缓存
✅ 收益 < 成本 → 性能反而下降
3️⃣ 破坏代码可读性
滥用后的代码:
scss
const handleSubmit = useCallback(() => {
doSomething();
}, []);
const data = useMemo(() => transform(props.data), [props.data]);
❌ 阅读成本变高
❌ 心智负担变重
❌ 后续维护者一脸懵
4️⃣ 依赖数组写错,Bug 更难查
scss
const handleClick = useCallback(() => {
setCount(count + 1);
}, []); // ❌ 闭包陷阱
- 忘记加依赖
- 拿到的是旧状态
- 调试极其痛苦
四、React 本身已经很快了
React 的 Fiber 架构 + 高效的 diff 算法,已经能处理绝大多数场景。
大多数组件 re-render 的成本,比你想象的要低得多
除非你遇到以下情况,否则不要急着优化:
- 列表长度 > 1000
- 复杂可视化 / 表格
- 频繁输入 / 拖拽
- 明显卡顿(用 Profiler 验证)
五、什么时候"必须"用?
✅ 场景一:配合 React.memo 使用
ini
const Child = React.memo(({ onClick }) => {
return <button onClick={onClick}>Click</button>;
});
const Parent = () => {
const handleClick = useCallback(() => {
// ...
}, []);
return <Child onClick={handleClick} />;
};
✅ 这是 useCallback 最经典、最正确的用法
✅ 场景二:昂贵的计算
ini
const sortedList = useMemo(() => {
return hugeList.sort(expensiveSort);
}, [hugeList]);
✅ 计算成本 >> 记忆化成本
✅ 场景三:作为其他 Hook 的依赖
scss
const fetchData = useCallback(async () => {
// ...
}, [token]);
useEffect(() => {
fetchData();
}, [fetchData]);
✅ 避免 effect 无限执行
六、什么时候"不要"用?
❌ 普通函数
css
const add = (a, b) => a + b;
✅ 没必要
❌ 简单对象
css
const style = useMemo(() => ({ color: 'red' }), []);
✅ 直接写更清晰
❌ 只在当前组件使用的函数
ini
const handleChange = useCallback(() => {
setValue(e.target.value);
}, []);
✅ 没有传给子组件,没收益
❌ 为了"看起来专业"
dart
// 不要这样
const isEven = useMemo(() => num % 2 === 0, [num]);
✅ 直接算更快
七、性能优化的正确顺序(非常重要)
✅ 第一步:让代码正确
✅ 第二步:让代码清晰
✅ 第三步:用 Profiler 找到瓶颈
✅ 第四步:针对性优化
不要在没有性能问题的时候"预防性优化"
八、一个真实的负优化案例
❌ 滥用前
javascript
function List({ items }) {
return items.map(item => <Item key={item.id} item={item} />);
}
❌ 滥用后
javascript
function List({ items }) {
const renderedItems = useMemo(() => {
return items.map(item => <Item key={item.id} item={item} />);
}, [items]);
return renderedItems;
}
问题:
useMemo每次都要 diffitems- JSX 本身创建成本极低
- 反而更慢 + 更难读
✅ 直接 return 才是正确做法
九、一句话总结
useMemo和useCallback是逃生舱 ,不是默认配置。
它们不是用来让代码"更快"的,而是用来避免不必要的 re-render 的。
记住这条铁律:
如果你不能用 React DevTools Profiler 证明它有性能问题,就不要优化。
十、最佳实践口诀
✅ 能不用就不用
✅ **传给子组件才考虑 useCallback**
✅ **计算很重才用 useMemo**
✅ 先测量,再优化
✅ 可读性永远优先于微优化