在最近的项目代码走查中,我发现一个现象:
很多同事几乎 「条件反射式地给所有函数包一层 useCallback
」 ,或者 「看到复杂计算就顺手套个 useMemo
」。
表面上看,这样写好像更「规范」,但实际上很多地方完全没必要,甚至可能导致性能变差。于是我决定写下这篇文章,聊聊 useCallback
/ useMemo
什么时候用才有效。
先复习一下它们的作用
-
useCallback(fn, deps)
👉 返回一个稳定的函数引用。
- 常用场景:把函数传给子组件,避免子组件不必要的重渲染。
-
useMemo(factory, deps)
👉 缓存一个计算结果。
- 常用场景:某个计算量大、依赖不常变的值。
一句话总结:
它们本身并不会提升性能,只有在「依赖变化频率低 + 使用场景敏感」时才有价值。
常见的误区
1. 「所有函数都要用 useCallback」
很多同学担心函数每次 render 都会重新创建,所以选择「一律用 useCallback 包裹」。
⚠️ 这是个误区。函数创建本身的开销极小,远远小于 React 内部维护依赖数组、闭包和 GC 的开销。
如果这个函数只是组件内部用,不会传递给子组件,那么用 useCallback
完全是 浪费。
2. 「计算过程都要用 useMemo」
有人觉得「计算逻辑里有数组 map
或对象处理」就必须用 useMemo
,否则会影响性能。
⚠️ 其实大部分计算(比如几百个元素的 map
)在 JS 里是毫秒级的,远远小于 React 组件更新的渲染耗时。
真正需要 useMemo
的场景是:
- 计算量大(比如复杂的排序、深度递归、正则批量匹配)。
- 依赖不常变(比如固定的配置项、缓存数据)。
什么时候才是「有效使用」?
可以用一句话来判断:
当它们能减少「不必要的子组件重渲染」或「避免大计算的重复执行」时,才是有效的。
✅ 适合用 useCallback
的场景
- 把函数作为 props 传递给
React.memo
包裹的子组件。 - 把函数作为 依赖 传递给另一个
useEffect
/useMemo
。
例子:
tsx
const Child = React.memo(({ onClick }) => {
console.log("child render");
return <button onClick={onClick}>click</button>;
});
export default function App() {
const [count, setCount] = useState(0);
// ✅ 用 useCallback 避免 Child 每次都重新渲染
const handleClick = useCallback(() => setCount(c => c + 1), []);
return <Child onClick={handleClick} />;
}
✅ 适合用 useMemo
的场景
- 渲染时需要执行 复杂计算,而依赖项很少变化。
- 需要生成一个稳定的 引用值(对象/数组),否则子组件会重复渲染。
tsx
const Child = React.memo(({ options }) => {
console.log("child render");
return <div>{options.join(",")}</div>;
});
export default function App() {
const [count, setCount] = useState(0);
// ✅ 用 useMemo 避免 options 每次 render 都新建数组
const options = useMemo(() => ["A", "B", "C"], []);
return (
<>
<button onClick={() => setCount(c => c + 1)}>add</button>
<Child options={options} />
</>
);
}
总结:三句话原则
- 不要滥用 :不是所有函数/计算都需要
useCallback
/useMemo
。 - 关注引用稳定性:只有在「传给子组件」或「作为依赖」时才必要。
- 成本大于收益:记住 Hook 本身也有开销,乱用可能比不用更慢。
写在最后
写这篇文章的原因,是因为我看到项目里出现了很多 无效的 useCallback
/ useMemo
:
- 所有函数一律包裹
useCallback
; - 明明是轻量计算却强行
useMemo
。
这不仅让代码变得冗余,还可能误导新人。希望大家在用它们时,先想一想:
👉 我这样写真的能减少渲染开销吗?
👉 如果去掉它,渲染会不会有性能问题?
如果答案是否定的,那大概率是不需要的。