React 性能优化利器:深入理解 `useCallback`、`useMemo` 与 `React.memo`

React 性能优化利器:深入理解 useCallbackuseMemoReact.memo

在 React 应用开发中,随着组件树的不断壮大,不必要的重复渲染 往往成为性能瓶颈的罪魁祸首。本文将从一个实际 Demo 出发,由浅入深地剖析 React 为性能优化而生的三个核心 API------React.memouseCallbackuseMemo


一、问题的起源:谁在"悄悄"重渲染?

想象这样一个场景:

javascript 复制代码
function App() {
  const [count, setCount] = useState(0);
  const [name, setName]   = useState('开心');

  return (
    <>
      <button onClick={() => setCount(count + 1)}>点击计数 {count}</button>
      <button onClick={() => setName('不开心')}>摸摸你</button>
      <RegularChild name={name} />
      <MemoChild name={name} />
    </>
  );
}

父组件 App 同时管理着 countname 两个状态。点击「点击计数」按钮时,只有 count 发生了变化,name 依然是 '开心'

但问题来了 ------此时 <RegularChild name={name} /><MemoChild name={name} /> 传入的 name 值并没有变,它们还有必要跟着父组件一起重渲染吗?

答案显然是否定的。这正是性能浪费的起点。


二、第一道防线:React.memo------"请记住我"

React.memo 是一个高阶组件(HOC) ,它的职责很简单:

对传入的 props 进行浅比较,如果 props 没有变化,就跳过该组件的重新渲染,直接复用上一次的结果。

来看 Demo 中的对比:

javascript 复制代码
// ❌ 普通组件------父组件每次渲染,它都跟着渲染
function RegularChild({ name }) {
  console.log('渲染了 RegularChild 组件');
  return <h2>{name}</h2>;
}

// ✅ Memo 组件------props 不变,它就不渲染
const MemoChild = memo(({ name }) => {
  console.log('MemoChild 组件渲染了');
  return <div>Hello, {name}</div>;
});

行为对比:

操作 RegularChild MemoChild
点击「点击计数」(count 变,name 不变) 🔴 重渲染 🟢 不渲染
点击「摸摸你」(name 变) 🔴 重渲染 🟡 重渲染(因为 props 确实变了)

效果立竿见影!memo 成功避免了 props 未变化时的无效渲染


三、memo 的"阿喀琉斯之踵"

memo 看起来很完美,但它有一个致命的盲区------引用类型 props

假设我们给 MemoChild 传递一个回调函数:

javascript 复制代码
function App() {
  const [count, setCount] = useState(0);
  const [name, setName]   = useState('开心');

  // 每次渲染都会创建一个全新的函数引用!
  const handleClick = () => {
    console.log('按钮被点击了');
  };

  return (
    <>
      <button onClick={() => setCount(count + 1)}>点击计数 {count}</button>
      <MemoChild name={name} onClick={handleClick} />
    </>
  );
}

现在再点击「点击计数」会发生什么?MemoChild 又重渲染了!

原因在于:App 每次渲染时,handleClick 都会被重新创建,它在内存中是一个全新的引用memo 做浅比较时发现 onClick 的引用变了,于是判定 props 已变化,照样触发渲染。

这就是 useCallback 登场的时刻。


四、useCallback------"函数引用,请保持稳定"

useCallback 的作用是缓存函数引用,只有当它的依赖项发生变化时,才返回一个新的函数引用。

javascript 复制代码
import { useCallback } from 'react';

function App() {
  const [count, setCount] = useState(0);
  const [name, setName]   = useState('开心');

  // ✅ count 不变,handleClick 的引用就不变
  const handleClick = useCallback(() => {
    console.log('当前 name:', name);
  }, [name]); // 依赖 name,name 变了才创建新函数

  return (
    <>
      <button onClick={() => setCount(count + 1)}>点击计数 {count}</button>
      <MemoChild name={name} onClick={handleClick} />
    </>
  );
}

现在点击「点击计数」时:count 变化 → name 没变 → handleClick 引用不变 → memo 浅比较通过 → MemoChild 不渲染! 🎉

核心心法:useCallback(fn, deps) = "帮我把这个函数缓存起来,除非 deps 里的值变了,否则永远给我同一个引用。"


五、useMemo------不止函数,值也可以缓存

useCallback 缓存的是函数 ,那如果我想缓存一个计算值呢?比如一个经过复杂计算得到的数组或对象?

scss 复制代码
// ❌ 每次渲染都重新计算,且生成全新引用
const sortedList = expensiveSort(items);

// ✅ 只有 items 变化时才重新计算
const sortedList = useMemo(() => expensiveSort(items), [items]);

useMemouseCallback 本质上是同一个东西:

scss 复制代码
useCallback(fn, deps)    // 等价于
useMemo(() => fn, deps)
Hook 缓存什么 返回值
useCallback 函数本身 缓存的函数
useMemo 函数的执行结果 缓存的

六、三者协作全景图

让我们把 memouseCallbackuseMemo 串联起来看一个完整示例:

javascript 复制代码
import { useState, useCallback, useMemo, memo } from 'react';

// Step 1:用 memo 包裹子组件,给浅比较打底
const ExpensiveList = memo(({ items, onItemClick }) => {
  console.log('ExpensiveList 渲染了');
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.label}
        </li>
      ))}
    </ul>
  );
});

function App() {
  const [count, setCount]   = useState(0);
  const [filter, setFilter] = useState('');
  const rawData             = ['React', 'Vue', 'Angular', 'Svelte'];

  // Step 2:用 useMemo 缓存计算结果(数组引用稳定)
  const filteredItems = useMemo(
    () => rawData
      .filter(d => d.toLowerCase().includes(filter.toLowerCase()))
      .map((d, i) => ({ id: i, label: d })),
    [filter] // 只有 filter 变了才重新计算
  );

  // Step 3:用 useCallback 缓存回调函数(函数引用稳定)
  const handleItemClick = useCallback((id) => {
    console.log('选中了:', id);
  }, []); // 空依赖 ------ 这个函数永远不会变

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        计数:{count}
      </button>
      <input
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="搜索..."
      />
      <ExpensiveList items={filteredItems} onItemClick={handleItemClick} />
    </>
  );
}

各司其职:

  • memo → 组件级别的渲染守卫
  • useMemo → 计算值的引用稳定器
  • useCallback → 函数引用的稳定器

当点击「计数」按钮时,count 变了但 filter 没变 → filteredItems 引用不变,handleItemClick 引用不变 → memo 判定 props 无变化 → ExpensiveList 不渲染。 完美!


七、注意事项 & 最佳实践

  1. 不是所有组件都需要 memo 如果组件本身很轻量,或者 props 总是在变,加上 memo 反而多了一次浅比较的开销。先测量,再优化。
  2. 依赖数组不要撒谎。 useCallback/useMemo 的依赖数组必须诚实列出所有在回调中用到的、会随时间变化的外部变量,否则你会拿到过期的闭包值(stale closure)。
  3. 不要过早优化。 Kent C. Dodds 有句名言:"直到你实际测量出性能问题之前,不要使用 useCallbackuseMemo。"------这句话值得玩味,但当你传递回调给 memo 子组件时,useCallback必要的配合,而非过早优化。
  4. useMemo 不保证永远不重新计算。 React 保留在未来某些场景下"丢弃"缓存值并重新计算的权力。所以不要把副作用逻辑写在 useMemo

📚 知识点总结

知识点 核心要点
问题根源 父组件状态更新 → 父组件渲染 → 所有子组件默认跟着渲染,即使传入的 props 没变
React.memo 高阶组件,对 props 做浅比较,props 不变则跳过子组件渲染。「记住」组件的渲染结果
memo 的局限 浅比较无法识别「值相等但引用不同」的情况。函数、对象、数组每次渲染都是新引用
useCallback(fn, deps) 缓存函数引用 ,仅在依赖项变化时返回新函数。是 memo 的最佳拍档
useMemo(() => val, deps) 缓存计算结果(值引用),仅在依赖项变化时重新计算。用于稳定对象/数组引用或避免昂贵计算
useCallback vs useMemo useCallback(fn, deps)useMemo(() => fn, deps) ------ 前者是后者的语法糖
三者协作 memo 做渲染守卫 → useMemo 稳定值引用 → useCallback 稳定函数引用,形成完整的性能优化链条
依赖数组规则 必须诚实包含回调中使用的所有外部可变值,否则产生过期闭包 Bug
优化原则 先测量再优化;传递引用类型 props 给 memo 组件时,useCallback/useMemo 是必要配合

生命在于运动,React 在于"不动"------让不需要渲染的组件安静地待着,才是对性能最大的尊重。 🧘

相关推荐
笃行3505 小时前
使用Rokid AIUI做了一个童年推箱子游戏
前端
excel6 小时前
前端加密的作用与使用场景
前端
计算机魔术师7 小时前
纽约时报版权诉讼披露:微软高管内部称训练 AI 是人类历史上最大规模劳动窃取
前端
去伪存真8 小时前
大模型的参数量为什么那么大?
前端·人工智能
IT_陈寒8 小时前
Java空指针这次真把我坑惨了
前端·人工智能·后端
八荒启·交互动画9 小时前
# Web特效020—让 Web 特效真正动起来:时间循环应该怎么接
前端·webgl·网页特效·八荒启-交互动画·八荒启
动恰客流统计9 小时前
线下零售数字化浪潮下,客流统计的3个核心发展趋势
大数据·前端·人工智能
用户938515635079 小时前
从小米前端面试题,彻底搞懂闭包、作用域链与 useState 惰性初始化
前端·面试
粥里有勺糖10 小时前
视野修炼第133期 | Native 回春了?
前端·github·agent
aichitang202411 小时前
前端小skill
前端·人工智能·算法·ai·前端框架