拒绝“无效焦虑”:为什么你 80% 的 useMemo 都在做负优化?

前言:一种名为"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]);

实际运行时发生了什么:

  1. 匿名函数 () => { ... } 被创建出来(没错,每次 Render 都会创建!这是 JS 语法决定的)。
  2. React 把这个新函数拿在手里。
  3. React 去对比 [a, b] 和上一次的依赖是不是一样。
  4. 如果一样,React 扔掉 刚才创建的新函数,把旧函数给你。
  5. 如果不一样,React 记下新函数,把旧函数扔掉。

发现了吗? 用了 useCallback,不仅函数照样创建,还多出了"创建数组"、"对比依赖"、"内存引用"这些额外的开销。

如果你只是为了给一个普通的 div 或者 button 传事件,这简直就是脱裤子放屁,多此一举

真相二:大部分 useMemo 都在亏本

再来看看 useMemo。它的初衷是缓存昂贵的计算结果。

但什么是"昂贵"?

  • ❌ 过滤一个长度为 50 的数组。
  • ❌ 拼接两个字符串。
  • array.map 转换一下数据结构。

现代浏览器的 JS 引擎(V8)快得离谱。遍历几千条数据也就是微秒级别的事。

而你为了省这点时间,写了这么一坨:

// 复制代码
const sortedList = useMemo(() => {
  return list.sort((a, b) => a.id - b.id);
}, [list]);

成本分析:

  1. React 内部要分配内存给 Hook 链表。
  2. 要保存 list 的引用。
  3. 每次 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 也要多干点活来维护这些缓存。

极简原则:

  1. 默认不用 useMemouseCallback
  2. 只要没卡顿,就别优化
  3. 只有 当你要传给 React.memo 子组件,或者作为 useEffect 依赖时,再把它们请出来。

把精力花在业务逻辑用户体验上,而不是花在帮 React 引擎做微不足道的内存管理上。

好了,我要去把那个满屏都是 useCallback 的文件重构了,愿天堂没有依赖数组。


下期预告:你真的需要 Redux/Zustand 吗?为什么说 90% 的前端项目其实只需要 React Query + Context 就够了?下一篇,我们来聊聊**"状态管理的极简主义"**,带你扔掉那些臃肿的 store。

相关推荐
重铸码农荣光1 小时前
从零实现一个「就地编辑」组件:深入理解 OOP 封装与复用的艺术
前端·javascript·前端框架
攻心的子乐1 小时前
redission 分布式锁
前端·bootstrap·mybatis
品克缤1 小时前
vue项目配置代理,解决跨域问题
前端·javascript·vue.js
m0_740043731 小时前
Vue简介
前端·javascript·vue.js
我叫张小白。1 小时前
Vue3 v-model:组件通信的语法糖
开发语言·前端·javascript·vue.js·elementui·前端框架·vue
undsky1 小时前
【RuoYi-SpringBoot3-UniApp】:一套代码,多端运行的移动端开发方案
前端·uni-app
FanetheDivine1 小时前
Next.js 学习笔记5 使用心得
react.js·next.js
Tonychen1 小时前
【React 源码阅读】useEffectEvent 详解
前端·react.js·源码
天天向上10241 小时前
vue3 封装一个在el-table中回显字典的组件
前端·javascript·vue.js