🚀 一文吃透 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. 权衡取舍:优化本身也有成本(内存、代码复杂度)
相关推荐
长存祈月心5 小时前
Rust 迭代器适配器
java·服务器·前端
先树立一个小目标6 小时前
puppeteer生成PDF实践
前端·javascript·pdf
冲刺逆向6 小时前
【js逆向案例二】瑞数6 深圳大学某医院
前端·javascript·vue.js
啃火龙果的兔子6 小时前
Promise.all和Promise.race的区别
前端
马达加斯加D6 小时前
Web身份认证 --- OAuth授权机制
前端
2401_837088506 小时前
Error:Failed to load resource: the server responded with a status of 401 ()
开发语言·前端·javascript
全栈师6 小时前
LigerUI下frm与grid的交互
java·前端·数据库
叫我詹躲躲6 小时前
被前端存储坑到崩溃?IndexedDB 高效用法帮你少走 90% 弯路
前端·indexeddb
无尽夏_6 小时前
CSS3(前端基础)
前端·css·1024程序员节