🚀 一文吃透 React 性能优化三剑客:useCallback、useMemo 与 React.memo

如果你学 React 一段时间了,肯定听过这三兄弟的名字:

  • useMemo
  • useCallback
  • React.memo

但你可能也有这样的疑惑:

🤔 它们都能防止无意义渲染,到底有什么区别?

🤔 为什么我用了 React.memo 子组件还是重新渲染?

🤔 什么时候用 useMemo,什么时候用 useCallback

别急,这篇文章会用最清晰的逻辑 + 直观的代码案例,帮你从渲染底层机制彻底吃透这三者。


🧩 一、React 性能问题的根源

1.1 为什么子组件会"无辜"重渲染?

在 React 中,每当父组件状态更新时,整个函数体会重新执行一遍:

jsx 复制代码
function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => console.log('clicked');

  console.log('Parent 渲染');
  
  return (
    <>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </>
  );
}

function Child({ onClick }) {
  console.log('Child 渲染');
  return <button onClick={onClick}>子按钮</button>;
}

执行流程:

  1. 点击 +1 按钮
  2. count 更新,触发 Parent 重新执行
  3. handleClick 被重新创建(新的函数引用)
  4. React 对比 Child 的 props:onClick 引用变了
  5. 触发 Child 重新渲染 ❌

问题关键: 在 JavaScript 中,每次创建的函数都是新的引用:

javascript 复制代码
const fn1 = () => {};
const fn2 = () => {};
console.log(fn1 === fn2); // false

即使函数内容完全一样,React 也会认为 props 发生了变化。


⚙️ 二、React.memo:阻止"假变化"引发的渲染

2.1 什么是 React.memo?

React.memo 是一个高阶组件(HOC) ,它会对组件的 props 进行浅比较

jsx 复制代码
const Child = React.memo(function Child({ onClick }) {
  console.log('🎨 Child 渲染');
  return <button onClick={onClick}>子按钮</button>;
});

工作原理:

ini 复制代码
父组件重渲染
   ↓
React.memo 检查
   ↓
判断:新 props === 旧 props ?
   ├─ 是 → ✅ 跳过渲染,复用上次结果
   └─ 否 → ❌ 执行渲染

2.2 React.memo 的局限

⚠️ 注意: React.memo 只做浅比较,对于引用类型(函数、对象、数组)的变化非常敏感:

jsx 复制代码
// ❌ 每次都是新引用,React.memo 失效
<Child onClick={() => {}} />
<Child data={{ name: 'Alice' }} />
<Child list={[1, 2, 3]} />

这就是为什么我们需要 useCallbackuseMemo


🔁 三、useCallback:稳定函数引用

3.1 基本用法

useCallback 返回一个记忆化的函数,只有当依赖项改变时才会更新:

jsx 复制代码
const handleClick = useCallback(() => {
  console.log('clicked');
}, []); // 依赖为空,函数引用永远不变

语法:

javascript 复制代码
const memoizedCallback = useCallback(
  () => {
    // 你的函数逻辑
  },
  [依赖项]
);

3.2 配合 React.memo 使用

jsx 复制代码
const Child = React.memo(({ onClick }) => {
  console.log('🎨 Child 渲染');
  return <button onClick={onClick}>子按钮</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  
  // ✅ 函数引用稳定,不会触发 Child 重渲染
  const handleClick = useCallback(() => {
    console.log('clicked');
  }, []);
  
  return (
    <>
      <p>Count: {count}</p>
      <Child onClick={handleClick} />
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </>
  );
}

执行结果:

  • 点击 +1 → Parent 渲染 ✅
  • Child 不再渲染 ✅(因为 onClick 引用未变)

3.3 带依赖的 useCallback

jsx 复制代码
const [userId, setUserId] = useState(1);

const fetchUserData = useCallback(() => {
  fetch(`/api/user/${userId}`);
}, [userId]); // 只有 userId 变化时才重新创建

💡 四、useMemo:缓存计算结果

4.1 与 useCallback 的区别

对比项 useCallback useMemo
缓存对象 函数本身 函数的返回值
返回值 () => {...} 执行结果
典型场景 传递给子组件的回调 复杂计算、派生状态

记忆技巧:

javascript 复制代码
useCallback(fn, deps)    ≈  useMemo(() => fn, deps)

4.2 实际应用场景

jsx 复制代码
function ProductList({ products }) {
  const [filter, setFilter] = useState('');

  // ✅ 只在 products 或 filter 改变时重新计算
  const filteredProducts = useMemo(() => {
    console.log('🔍 执行过滤逻辑...');
    return products.filter(p => 
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]);

  return (
    <>
      <input 
        value={filter} 
        onChange={e => setFilter(e.target.value)} 
      />
      {filteredProducts.map(p => (
        <div key={p.id}>{p.name}</div>
      ))}
    </>
  );
}

性能对比:

场景 无 useMemo 有 useMemo
1000 个商品首次渲染 计算 1 次 计算 1 次
输入框 focus(无关状态变化) 计算 1 次 ❌ 0 次 ✅
修改 filter 计算 1 次 计算 1 次

4.3 缓存对象/数组给子组件

jsx 复制代码
const Child = React.memo(({ config }) => {
  console.log('🎨 Child 渲染');
  return <div>{config.theme}</div>;
});

function Parent() {
  const [count, setCount] = useState(0);
  
  // ✅ config 引用稳定
  const config = useMemo(() => ({
    theme: 'dark',
    lang: 'zh-CN'
  }), []);
  
  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <Child config={config} />
    </>
  );
}

🧠 五、三者关系与配合策略

5.1 配合时序图

sequenceDiagram participant P as 父组件 participant CB as useCallback participant CM as useMemo participant C as 子组件 participant M as React.memo P->>P: state 更新 P->>CB: 检查依赖 CB-->>P: 返回缓存函数 ✅ P->>CM: 检查依赖 CM->>CM: 依赖未变,跳过计算 CM-->>P: 返回缓存值 ✅ P->>C: 传递 props C->>M: 触发渲染前检查 M->>M: Object.is(newProps, oldProps) alt props 引用相同 M-->>C: 跳过渲染 ✅ else props 引用不同 M->>C: 执行渲染 ❌ C-->>P: 返回 JSX end

5.2 三者对比表

名称 类型 缓存内容 返回值 是否阻止渲染 常见搭配
React.memo HOC 组件渲染结果 组件 ✅ 是 useCallback/useMemo
useCallback Hook 函数引用 函数 ❌ 否 React.memo
useMemo Hook 计算结果 任意值 ❌ 否 React.memo

🔍 六、最佳实践与常见误区

6.1 什么时候应该用?

场景 推荐方案 理由
子组件接收函数 props 频繁渲染 useCallback + React.memo 稳定引用 + 跳过渲染
大列表过滤/排序/计算 useMemo 避免重复计算
传递对象/数组给 memo 子组件 useMemo 稳定引用
组件接收的 props 不常变化 React.memo 减少渲染次数
简单组件、轻量计算 ❌ 不用 优化成本 > 收益

6.2 常见误区

❌ 误区 1:到处使用

jsx 复制代码
// ❌ 过度优化,反而增加开销
const add = useCallback((a, b) => a + b, []);
const result = useMemo(() => 1 + 1, []);

正确做法: 针对性能瓶颈优化,用 React DevTools Profiler 测量。

❌ 误区 2:忘记包裹 React.memo

jsx 复制代码
// ❌ 只用 useCallback 没用,子组件还会渲染
const handleClick = useCallback(() => {}, []);
<Child onClick={handleClick} /> // Child 没用 React.memo

❌ 误区 3:依赖项遗漏

jsx 复制代码
// ❌ 依赖了 count 但没声明,会导致闭包陷阱
const handleClick = useCallback(() => {
  console.log(count);
}, []); // 应该是 [count]

🎯 七、综合实战案例

jsx 复制代码
import React, { useState, useMemo, useCallback, memo } from "react";

// 子组件:展示计算结果
const ResultDisplay = memo(({ result }) => {
  console.log("🎨 ResultDisplay 渲染");
  return <div>计算结果:{result}</div>;
});

// 子组件:按钮
const ActionButton = memo(({ onAction, label }) => {
  console.log("🎨 ActionButton 渲染");
  return <button onClick={onAction}>{label}</button>;
});

export default function App() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  // ✅ useMemo 缓存复杂计算
  const expensiveResult = useMemo(() => {
    console.log("💡 执行昂贵计算...");
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += i;
    }
    return sum + count;
  }, [count]); // 只在 count 变化时重新计算

  // ✅ useCallback 缓存函数引用
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  const handleReset = useCallback(() => {
    setCount(0);
  }, []);

  console.log("🧩 App 组件渲染");

  return (
    <div>
      <h1>性能优化示例</h1>
      
      <p>计数:{count}</p>
      
      {/* 输入框变化不会触发子组件渲染 */}
      <input 
        value={text} 
        onChange={e => setText(e.target.value)} 
        placeholder="输入文字试试"
      />
      
      {/* 这两个组件只在必要时渲染 */}
      <ResultDisplay result={expensiveResult} />
      <ActionButton onAction={handleIncrement} label="+1" />
      <ActionButton onAction={handleReset} label="重置" />
    </div>
  );
}

执行效果:

  1. 修改输入框 → 只有 App 渲染,子组件不渲染 ✅
  2. 点击 +1 → AppResultDisplay 渲染,ActionButton 不渲染 ✅
  3. 点击重置 → 同上

🧠 八、记忆口诀(背下来你就是懂哥)

Hook/API 记忆口诀 核心功能 英文含义
useMemo "我记住" 缓存计算结果 memoize value
useCallback "我记住函数" 缓存函数引用 callback function
React.memo "我记住组件" 跳过重复渲染 memoize component

一句话总结:

useCallbackuseMemo 让 props 稳定,
React.memo 让稳定的 props 不触发渲染。


📊 九、性能优化决策树

markdown 复制代码
开始
 │
 ├─ 组件渲染慢吗?
 │   ├─ 否 → 不用优化
 │   └─ 是 ↓
 │
 ├─ 是子组件无意义渲染吗?
 │   ├─ 是 → 用 React.memo 包裹子组件
 │   │        ↓
 │   │      检查 props 类型
 │   │        ├─ 函数 → useCallback
 │   │        ├─ 对象/数组 → useMemo
 │   │        └─ 基本类型 → 不用处理
 │   │
 │   └─ 否 ↓
 │
 └─ 有复杂计算逻辑吗?
     ├─ 是 → 用 useMemo 缓存结果
     └─ 否 → 检查其他性能问题

🏁 十、结语

React 的性能优化不只是加几个 Hook 就完事,更重要的是理解渲染流程与引用稳定性的本质。

真正的优化思维:

  1. 测量优先:用 React DevTools Profiler 找到真正的瓶颈
  2. 针对性优化 :不是所有组件都需要 memo
  3. 理解原理:知道为什么要用,而不是盲目照搬
  4. 权衡取舍:优化本身也有成本(内存、代码复杂度)
相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax