React 性能优化之“死都不想白渲染”:React.memo 和 useCallback 全面详解

103 阅读4分钟

“为什么我点了个按钮,结果旁边的组件也跟着重新渲染了一下?”
“我不改它,它自己为什么刷新?”
React 初学者看到组件莫名其妙刷新,内心常常写满两个字:离谱。

没错,函数组件默认会在父组件更新时自动重新执行,即使它的 props 根本没变
这篇文章,我们就来好好唠唠 React 性能优化中的两个猛将:React.memouseCallback,让组件该刷就刷,不该刷死都别动。


一、React.memo:组件的记忆力增强药

我们先从 React.memo 讲起,它的任务很简单:记住上一次的 props,如果没变,就别重新渲染

1. 默认行为回顾

来看个例子:

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

  return (
    <>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Child />
    </>
  );
}

function Child() {
  console.log('Child 组件渲染了');
  return <div>我是子组件</div>;
}

你每点一次按钮,Child 都会重新执行。尽管它的 props 没有变化,但它还是被动刷新了。这就好像你喊了个室友起床,结果把全宿舍都吵醒了,冤不冤?

2. React.memo 的介入

const Child = React.memo(() => {
  console.log('Child 组件渲染了');
  return <div>我是子组件</div>;
});

加上 React.memo 后,只要 Child 的 props 没变,它就不会重新执行。

React.memo 的原理其实很简单:对 props 做浅比较,能复用就复用。

if (shallowEqual(prevProps, nextProps)) {
  skipRender();
} else {
  rerender();
}

你可以理解为:这是一个“只在必要时重渲染”的高阶组件。


二、useCallback:函数稳定器,别每次都造新函数

1. 问题场景:函数也是 props

继续看一个例子:

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

  const handleClick = () => {
    console.log('Button clicked');
  };

  return (
    <>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Button onClick={handleClick} />
    </>
  );
}

const Button = React.memo(({ onClick }) => {
  console.log('Button 渲染了');
  return <button onClick={onClick}>点我</button>;
});

你以为 ButtononClick 没变,结果还是每次都触发重渲染。为啥?因为函数是引用类型:

const handleClick = () => {}

这句代码每次执行都会返回一个新的函数对象,地址变了,React.memo 检测到 props 变了,自然就渲染了。

2. 引入 useCallback

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

useCallback 的作用是:缓存函数的引用地址,只有当依赖数组变化时才会重新生成。

你可以理解为: “用 useCallback 包过的函数,不改就别动”


三、React.memo + useCallback:拆不散的性能搭档

const Button = React.memo(({ onClick }) => {
  console.log('Button 渲染了');
  return <button onClick={onClick}>点我</button>;
});

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

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

  return (
    <>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <Button onClick={handleClick} />
    </>
  );
}

在这个组合拳中:

  • Button 使用 React.memo,只有 props 变化才渲染;
  • handleClick 使用 useCallback,保持引用不变;
  • 所以 Button 就不会重复刷新了。

四、useCallback 底层原理

你可能听说过一句话:

useCallback(fn, deps) 的本质就是 useMemo(() => fn, deps)

这是真的。它就是对函数的引用做了一层缓存,避免每次重新创建函数。

但它和 useMemo 有一个重要区别:

Hook缓存的是什么
useMemo计算结果
useCallback函数本身

五、你以为安全,实际错用的场景

1. 缓存了旧数据

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

这个写法看似没有问题,实际上它会永远打印初始值 0,因为 count 没在依赖里,函数不会重新生成。

修正:

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

要想用 useCallback,一定要搞清楚:它“记住”的是你定义函数的那一刻的上下文环境


六、什么时候该用 memo 和 useCallback?

别盲用。它们虽然听起来“高性能”,但也会消耗内存来做缓存和比较。

使用建议:

  • 子组件是函数组件
  • 子组件渲染频率高
  • 子组件 props 不常变
  • 子组件自身逻辑重 / 包含 expensive 运算

不用的场景:

  • 页面组件(只会初始化一次)
  • 简单组件、没有性能问题的部分
  • 父组件几乎不更新的地方

七、总结一波

工具干啥用的缓存内容推荐搭配
React.memo缓存组件propsuseCallback / useMemo
useCallback缓存函数引用函数本身React.memo
useMemo缓存计算值函数返回值高开销计算场景

八、彩蛋:React 到底怎么判断 props 有没有变?

React.memo 使用的是 Object.is(prevProps, nextProps) 做的浅层对比,你只要传的 props 是新对象(如函数、数组、对象),就会触发更新。

<Button config={{ color: 'red' }} />

这个写法,即使每次 config 内容不变,也会触发重渲染。解决方案:

const config = useMemo(() => ({ color: 'red' }), []);
<Button config={config} />

保持引用稳定,才能让 memo 发挥作用。


九、写在最后

React 的渲染机制本质上是“看起来很勤快,但有点过度热情”。我们要做的不是“强行制止它”,而是用更聪明的方式告诉它:

“别担心,我没变,不用你操心了。”

理解 React.memouseCallback 是写好 React 性能优化的第一步。
剩下的,就是你在项目里多用、多试、多踩坑,再回来看看这篇文章。