别再无脑加useCallback了

别再无脑加useCallback了

Sean 1 2026-04-11

今天来聊聊使用useCallback进行React优化中的翻车现场。

相信很多写React的伙伴在遇到页面卡顿,组件重复渲染时,第一时间想到的性能优化的方案就是使用useCallback包裹函数,React.memo包裹组件。

但是结果往往事与愿违,性能反而变差了,今天就我的理解来聊聊为什么会这样?React性能优化里的三剑客:React.memouseMemouseCallback,到底应该怎么使用,用来干什么的?

1.为什么加了memo,它还是在疯狂渲染?

我们来复现一个经典的踩坑现场。

先写一个子组件<Child />,每次渲染耗时1000ms。为了优化,加上React.memo,逻辑是:只要我Child数据没变,它就不会重新渲染。

const Child = React.memo(({ onClick }) => {
  const start = performance.now();
  while (performance.now() - start < 1000) {
  // 空转,阻塞 1 秒
  }
  const end = performance.now();
  const renderTime = (end - start).toFixed(2);

  const componentStyle = {
    border: '2px solid #000',
    padding: '20px',
    borderRadius: '8px',
    color: 'red',
    background: '#9c9c9cff'
  };

  return (
    <div style={componentStyle}>
      <h3></h3>
      <p>我是Child,每次渲染,耗时:{renderTime} ms</p>
      <button onClick={onClick} style={{padding: '5px 10px', cursor: 'pointer'}}>
        点我调用父组件传来的函数
      </button>
    </div>
  );
});

然后父组件中,调用子组件,并传入函数:

function Parent() {
  const [count, setCount] = useState(0);

  // 每次 Parent 渲染,都会生成一个全新的 handleClick 函数
  const handleClick = () => console.log('点我了')

  return (
    <div style={{ padding: '30px'}}>
      <h1>我是Parent</h1>
      <p>当前 Count: {count}</p>
      <button 
        onClick={() => setCount(count + 1)}
        style={{fontSize: '16px', cursor: 'pointer',border:'2px #000 solid '}}
      >
        点我更新父组件State
      </button>
      <hr style={{margin: '20px 0'}} />
      <Child onClick={handleClick} />
    </div>
  );
}

然后我们跑起来,抓一下渲染实况:

我们可以清楚看到子组件发生了重绘,且原因是点击了父组件按钮。

可是为什么子组件已经缓存,触发父组件函数更新State还会引发子组件渲染呢,其实这是因为React在比较props有无变化时,用的是浅比较。在JS中,每次Parent重新执行,传递的handleClick函数都会在内存中开辟一块全新的空间。

对于React.memo来说,至此传进来的函数地址和上次不一样,说明props变了,重新渲染!

(PS:这里其实涉及到了一点“值和引用”,关于JS如何对内存中的“栈(Stack)”和堆(Heap)进行分配,我们有时间可以单独拉一篇讲讲。)

2. 正确使用方式

为了解决上面这种因为内存“地址变化”导致的踩坑,我们应该正确使用缓存。

  • React.memo 高阶组件,只要props跟上次一模一样,我就不让你进去重新渲染。

  • useCallback :缓存函数。只要依赖项没变,就保证每次都是同一个内存地址的函数。

  • useMemo :缓存复杂计算结果或对象。只要依赖不变,每次返回的都是同一个对象地址。

所以,我们应该在Parent中这样解决:

// 必须把要传给 memo 组件的函数和对象,都缓存起来!
const handleClick = useCallback(() => console.log('点我了'), []);

// React.memo一看,地址还是上次那个,放行,不渲染!
<Child onClick={handleClick} />

3. 思考:那我们就应该把所有东西都缓存么?

当然不是!优化是有成本的。

useMemouseCallback 本身也是函数,每次渲染 React 都要去检查它们的依赖数组,还要去开辟空间把结果存起来。如果你在组件里写了满屏的 useCallback,可读性变差的同时,一个属性的意外变化,就会导致整个流程的崩溃:React.memo停止工作,useCallback变得毫无意义,useMemo再变得毫无意义。特别是props的传递,来自”propsprops“。

const Child = () => { };
const ChildMemo = React.memo(Child); 
const Component = (props) => { return < ChildMemo {...props} />; }; 
const ComponentWrapper= (props) => { return < Component {...props} />; }; 
const InitialComponent = (props) => { 
  // 此组件有状态 → 它会重新渲染 → 其下方的所有内容都会重新渲染
  return ( < ComponentWrapper { ...props } data = {{ id: ' 1 ' }} />   ); 
};

尤其是当这些组件位于不同文件,拥有自己的逻辑,且相当复杂时,任何一个层级的破坏,就会导致整个链路的崩溃。

那什么时候用呢?

  1. 传给memo组件的props:需要传递引用类型时,必须使用useCallbackuseMemo,不然memo就失效了。

  2. 耗时的计算:假设有一个上万条的数据需要做过滤、排序等操作时,果断用useMemo缓存,防止主线程卡死。

  3. 作为其他Hooks的依赖项:在useEffect中监听某个函数或者对象时,防止死循环,需要用缓存。

什么时候不用?

  1. 普通事件:如果只是一个简单的点击事件函数,就没有必要用useCallback了,原生DOM并不缺这点性能。

  2. 简单组件:使用缓存的目的是优化性能,在交互逻辑比较简单的组件中,就没有必要使用缓存了。

最后

React给我们使用的这些钩子,是为了在遇到真正的性能瓶颈时,能够有优雅的方法解决,滥用只会让代码变成屎山。

下次写代码时,遇到想敲useMemo的时候,不妨停下好好想一想:“这玩意真的需要缓存么?”

最后的最后,祝大家的代码少一点无意义的 re-render,多一点准点下班的快乐!