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 在于"不动"------让不需要渲染的组件安静地待着,才是对性能最大的尊重。 🧘

相关推荐
Yoram1 小时前
JavaScript 错误处理的一种整理与实践
前端
你听得到111 小时前
排查 App 问题:别只盯着报错,把前后发生的事情串起来
android·前端·flutter
lllsure1 小时前
Vue&React Router
前端·vue.js·react.js
weixin_431600441 小时前
为什么 Agent REPL 要上 Ink:好处、用法与内部设计
前端·学习·ai·agent·ai编程
IT_陈寒2 小时前
Java 8的stream让我debug了一整天,气笑了
前端·人工智能·后端
sugar__salt2 小时前
深入理解 React useContext:跨层级组件通信的核心利器
前端·javascript·react.js·前端框架·框架
小黑技术栈2 小时前
web前端基础到入门——14day
前端·数据库·oracle
谷哥的小弟2 小时前
TypeScript类型断言
前端·javascript·typescript
智塑未来2 小时前
鸿蒙系统小红书隐私保护——应用锁开启指南
服务器·前端·harmonyos