前言:一种名为"Hook 强迫症"的病
昨天 Code Review,看到刚入职的小伙子提交了一段代码,差点一口老血喷在屏幕上。
他写了一个简单的 <button onClick={handleClick}>,然后那个 handleClick 长这样:
tsx
const handleClick = useCallback(() => {
setOpen(true);
}, []);
问他:"为什么要把这个简单的 toggle 动作包在 useCallback 里?" 他一脸无辜地说:"为了性能优化啊!Eslint 也建议我加依赖啊!防止函数重新创建啊!"
兄弟们, "防止函数重新创建" 恐怕是 React 圈最大的谎言之一。
今天我们就来聊聊 React 性能优化的**"安慰剂效应"**,以及为什么你的优化代码可能比不优化还要慢。
真相一:useCallback 并不省内存
很多新手觉得,加了 useCallback,函数就不会被创建了。
错!大错特错!
我们来看一下 JavaScript 是怎么跑这段代码的:
//
const memoizedFunc = useCallback(() => { ... }, [a, b]);
实际运行时发生了什么:
- 匿名函数
() => { ... }被创建出来(没错,每次 Render 都会创建!这是 JS 语法决定的)。 - React 把这个新函数拿在手里。
- React 去对比
[a, b]和上一次的依赖是不是一样。 - 如果一样,React 扔掉 刚才创建的新函数,把旧函数给你。
- 如果不一样,React 记下新函数,把旧函数扔掉。
发现了吗? 用了 useCallback,不仅函数照样创建,还多出了"创建数组"、"对比依赖"、"内存引用"这些额外的开销。
如果你只是为了给一个普通的 div 或者 button 传事件,这简直就是脱裤子放屁,多此一举。
真相二:大部分 useMemo 都在亏本
再来看看 useMemo。它的初衷是缓存昂贵的计算结果。
但什么是"昂贵"?
- ❌ 过滤一个长度为 50 的数组。
- ❌ 拼接两个字符串。
- ❌
array.map转换一下数据结构。
现代浏览器的 JS 引擎(V8)快得离谱。遍历几千条数据也就是微秒级别的事。
而你为了省这点时间,写了这么一坨:
//
const sortedList = useMemo(() => {
return list.sort((a, b) => a.id - b.id);
}, [list]);
成本分析:
- React 内部要分配内存给 Hook 链表。
- 要保存
list的引用。 - 每次 Render 都要去对比
list引用有没有变。
结论: 如果不涉及大量的数学运算(比如加密算法、复杂的图形计算)或者数据量级没达到万级,直接裸写比用 useMemo 更快,内存占用更少,代码更可读。
那到底什么时候用?(黄金法则)
既然这么说,那这两个 Hook 删了算了? 别,它们是有用的,但只有在以下两种情况:
1. 维持"引用稳定性" (Referential Equality) ------ 这是重点!
这才是它们存在的真正意义。不是为了省计算,而是为了不坑队友。
场景 A:这个函数/对象是别人的 useEffect 依赖
const
useEffect(() => {
// 如果 params 不包 useMemo,这里就会无限死循环!
// 因为每次 render,params 都是个新对象
fetchData(params);
}, [params]);
场景 B:这个属性要传给被 React.memo 包裹的子组件
const
console.log("我只有在 onClick 引用变了才会重渲染");
return <button onClick={onClick}>点我</button>;
});
const Parent = () => {
// 这里必须用 useCallback!
// 否则 Parent 每次重渲染,生成新的 handleClick
// 导致 HeavyChild 以为 props 变了,React.memo 就失效了!
const handleClick = useCallback(() => {}, []);
return <HeavyChild onClick={handleClick} />;
};
记住:如果接受 props 的子组件没有被 React.memo 包裹,那你父组件里的 useCallback 就是在做无用功。
2. 真正的昂贵计算
什么算昂贵?如果在这个计算过程中,你的页面明显卡了一下(丢帧),那就加上 useMemo。否则,相信 V8 引擎,它比你想象的要强大。
总结:别当"过度优化"的受害者
我们写代码往往有一种心理安慰:看到绿色的 Check,看到代码里充满了 useMemo,就觉得自己写出了高性能代码,这就是性能优化的安慰剂效应。
实际上,你只是把代码变得更难读了,并且让 React 也要多干点活来维护这些缓存。
极简原则:
- 默认不用
useMemo和useCallback。 - 只要没卡顿,就别优化。
- 只有 当你要传给
React.memo子组件,或者作为useEffect依赖时,再把它们请出来。
把精力花在业务逻辑 和用户体验上,而不是花在帮 React 引擎做微不足道的内存管理上。
好了,我要去把那个满屏都是 useCallback 的文件重构了,愿天堂没有依赖数组。
下期预告:你真的需要 Redux/Zustand 吗?为什么说 90% 的前端项目其实只需要 React Query + Context 就够了?下一篇,我们来聊聊**"状态管理的极简主义"**,带你扔掉那些臃肿的 store。
