为什么不要滥用 useMemo 和 useCallback?过度优化反而更卡

7 阅读3分钟

为什么不要滥用 useMemo 和 useCallback?过度优化反而更卡

在 React 性能优化中,useMemo 和 useCallback 是最容易被误解和滥用的两个 Hook。

很多开发者一看到“性能优化”就本能地给所有函数、对象、计算结果包一层 useMemo / useCallback,结果却是:代码变复杂了,性能反而变差了。

这篇文章会讲清楚:

  • useMemo / useCallback 到底在解决什么问题
  • 为什么“乱用”会导致负优化
  • 什么时候该用,什么时候不该用
  • 正确的性能优化心智模型

一、先搞懂:useMemo 和 useCallback 是干什么的?

useMemo:缓存“值”

const computedValue = useMemo(() => {
  return heavyCompute(a, b);
}, [a, b]);

✅ 只在 a 或 b 变化时,才重新计算


useCallback:缓存“函数引用”

const handleClick = useCallback(() => {
  console.log(a);
}, [a]);

等价于:

const handleClick = useMemo(() => {
  return () => console.log(a);
}, [a]);

✅ 函数引用不变,避免子组件不必要的重新渲染


二、它们解决的核心问题只有一个

防止“引用类型”频繁变化,导致子组件不必要地 re-render

问题示例(没有 useCallback)

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)

const handleClick = useCallback(() => {
  console.log('click');
}, []);

✅ 函数引用稳定

✅ Child 只有在真正需要时才渲染


三、为什么“滥用”反而更慢?

1️⃣ useMemo / useCallback 本身有成本

它们不是“免费”的,内部做了这些事情:

  • 记录上一次的依赖数组
  • 每次 render 进行浅比较
  • 维护缓存结构

一句话:记忆化本身也是开销


2️⃣ 过早优化 = 负优化

const obj = useMemo(() => ({ a: 1 }), []);

这个对象创建成本极低,但:

  • React 每次 render 都要比较依赖数组
  • 还要维护缓存

✅ 收益 < 成本 → 性能反而下降


3️⃣ 破坏代码可读性

滥用后的代码:

const handleSubmit = useCallback(() => {
  doSomething();
}, []);

const data = useMemo(() => transform(props.data), [props.data]);

❌ 阅读成本变高

❌ 心智负担变重

❌ 后续维护者一脸懵


4️⃣ 依赖数组写错,Bug 更难查

const handleClick = useCallback(() => {
  setCount(count + 1);
}, []); // ❌ 闭包陷阱
  • 忘记加依赖
  • 拿到的是旧状态
  • 调试极其痛苦

四、React 本身已经很快了

React 的 Fiber 架构 + 高效的 diff 算法,已经能处理绝大多数场景。

大多数组件 re-render 的成本,比你想象的要低得多

除非你遇到以下情况,否则不要急着优化:

  • 列表长度 > 1000
  • 复杂可视化 / 表格
  • 频繁输入 / 拖拽
  • 明显卡顿(用 Profiler 验证)

五、什么时候“必须”用?

✅ 场景一:配合 React.memo 使用

const Child = React.memo(({ onClick }) => {
  return <button onClick={onClick}>Click</button>;
});

const Parent = () => {
  const handleClick = useCallback(() => {
    // ...
  }, []);

  return <Child onClick={handleClick} />;
};

✅ 这是 useCallback 最经典、最正确的用法


✅ 场景二:昂贵的计算

const sortedList = useMemo(() => {
  return hugeList.sort(expensiveSort);
}, [hugeList]);

✅ 计算成本 >> 记忆化成本


✅ 场景三:作为其他 Hook 的依赖

const fetchData = useCallback(async () => {
  // ...
}, [token]);

useEffect(() => {
  fetchData();
}, [fetchData]);

✅ 避免 effect 无限执行


六、什么时候“不要”用?

❌ 普通函数

const add = (a, b) => a + b;

✅ 没必要


❌ 简单对象

const style = useMemo(() => ({ color: 'red' }), []);

✅ 直接写更清晰


❌ 只在当前组件使用的函数

const handleChange = useCallback(() => {
  setValue(e.target.value);
}, []);

✅ 没有传给子组件,没收益


❌ 为了“看起来专业”

// 不要这样
const isEven = useMemo(() => num % 2 === 0, [num]);

✅ 直接算更快


七、性能优化的正确顺序(非常重要)

✅ 第一步:让代码正确

✅ 第二步:让代码清晰

✅ 第三步:用 Profiler 找到瓶颈

✅ 第四步:针对性优化

不要在没有性能问题的时候“预防性优化”


八、一个真实的负优化案例

❌ 滥用前

function List({ items }) {
  return items.map(item => <Item key={item.id} item={item} />);
}

❌ 滥用后

function List({ items }) {
  const renderedItems = useMemo(() => {
    return items.map(item => <Item key={item.id} item={item} />);
  }, [items]);

  return renderedItems;
}

问题:

  • useMemo 每次都要 diff items
  • JSX 本身创建成本极低
  • 反而更慢 + 更难读

✅ 直接 return 才是正确做法


九、一句话总结

useMemo 和 useCallback 是逃生舱,不是默认配置。

它们不是用来让代码“更快”的,而是用来避免不必要的 re-render​ 的。

记住这条铁律:

如果你不能用 React DevTools Profiler 证明它有性能问题,就不要优化。


十、最佳实践口诀

✅ 能不用就不用​

✅ **传给子组件才考虑 useCallback**​

✅ **计算很重才用 useMemo**​

✅ 先测量,再优化​

✅ 可读性永远优先于微优化