React useCallback 与 useMemo 性能优化完全指南

小飞兽 React 491 次阅读 2026-05-24

React useCallback 与 useMemo 性能优化完全指南

一、Introduction

React 应用性能问题的根源,往往在于不必要的重新渲染(re-render):父组件状态变化导致子组件也跟着重新渲染,即使子组件的 props 根本没变。这时候 useCallback 和 useMemo 就派上用场了——它们通过缓存函数和计算结果,让组件在 props 未变化时跳过重新渲染。

但注意:不是所有组件都需要 memoization。过度使用这两个 Hook 会增加内存负担和维护复杂度。什么时候该用、什么时候不该用,是本文要解答的核心问题。

---

二、useCallback:缓存回调函数

#### 2.1 为什么要缓存回调函数?

当一个函数作为 props 传给子组件时,如果每次渲染都创建新函数,即使函数逻辑完全相同,子组件也会认为 props 变了(因为引用不同),从而触发不必要的重新渲染。

import React, { useState, useCallback } from 'react';

// 子组件:接受 onIncrement 作为 prop
function CounterButton({ onIncrement, label }) {
  console.log(`Button [${label}] rendered`);
  return <button onClick={onIncrement}>{label}</button>;
}

// 用 React.memo 包装,让子组件在 props 不变时跳过渲染
const MemoizedButton = React.memo(CounterButton);

function Parent() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  // 不用 useCallback:每次渲染都创建新函数,导致 MemoizedButton 重新渲染
  const handleIncrementNaive = () => {
    setCount(c => c + step);
  };

  // 用 useCallback:只有在 step 变化时才创建新函数
  // count 变化时,handleIncrement 保持同一个引用
  const handleIncrementOptimized = useCallback(() => {
    setCount(c => c + step);
  }, [step]);

  return (
    <div>
      <p>计数:{count},步长:{step}</p>
      {/* 每次 count 变化,handleIncrementNaive 都是新引用
          → 即使 MemoizedButton 也会重新渲染(除非你用 React.memo) */}
      <MemoizedButton
        onIncrement={handleIncrementNaive}
        label="普通方式"
      />

      {/* handleIncrementOptimized 只有 step 变化时才变
          → MemoizedButton 可以跳过不必要的渲染 */}
      <MemoizedButton
        onIncrement={handleIncrementOptimized}
        label="优化方式"
      />

      <button onClick={() => setStep(s => s + 1)}>增加步长</button>
    </div>
  );
}

export default Parent;

---

三、useMemo:缓存计算结果

#### 3.1 基础用法

useMemo 用于缓存昂贵计算的结果,避免每次渲染都重新计算:

import React, { useState, useMemo } from 'react';

function ExpensiveCalculation() {
  const [number, setNumber] = useState(5);
  const [multiplier, setMultiplier] = useState(2);

  // 模拟一个昂贵的计算(实际项目中可能是复杂的排序、筛选、大数据处理)
  const computeFactorial = (n) => {
    console.log('执行昂贵计算...');
    if (n <= 1) return 1;
    let result = 1;
    for (let i = 2; i <= n; i++) {
      result *= i;
    }
    return result;
  };

  // 不用 useMemo:number 或 multiplier 任何一个变化,都会重新计算
  const factorialNaive = computeFactorial(number);

  // 用 useMemo:只有 number 变化时才重新计算
  // multiplier 变化不会触发 factorialMemo 重新计算
  const factorialMemo = useMemo(() => {
    console.log('useMemo 计算...');
    return computeFactorial(number);
  }, [number]);

  return (
    <div>
      <p>数字:{number},乘数:{multiplier}</p>
      <p>普通计算 factorial({number}) = {factorialNaive}</p>
      <p>useMemo 缓存 factorial({number}) = {factorialMemo}</p>

      <button onClick={() => setNumber(n => Math.max(1, n - 1))}>数字-1</button>
      <button onClick={() => setNumber(n => n + 1)}>数字+1</button>
      <button onClick={() => setMultiplier(m => m + 1)}>乘数+1(只影响显示,不重新计算 factorial)</button>
    </div>
  );
}

export default ExpensiveCalculation;

---

四、useCallback 与 useMemo 的正确使用场景

#### 4.1 性能分析先行,不要猜测

React 官方推荐先用 React DevTools Profiler 或 Chrome Performance 标签进行性能分析,确认确实存在性能问题后再使用这些优化 Hook。过度优化反而浪费资源。

#### 4.2 什么时候该用

// 场景一:回调函数作为子组件的 props,且子组件用 React.memo 包装
const handleSubmit = useCallback(() => {
  submitForm(data);
}, [data]);

// 场景二:useEffect 的依赖是函数,用 useCallback 包裹确保依赖稳定
const handleDataChange = useCallback((newData) => {
  setData(processData(newData));
}, []);

useEffect(() => {
  window.addEventListener('resize', handleDataChange);
  return () => window.removeEventListener('resize', handleDataChange);
}, [handleDataChange]);

// 场景三:渲染大量数据时的派生计算(排序、筛选、聚合)
const sortedList = useMemo(() => {
  return [...items].sort((a, b) => a.price - b.price);
}, [items]);

const filteredList = useMemo(() => {
  return items.filter(item => item.category === selectedCategory);
}, [items, selectedCategory]);

#### 4.3 什么时候不该用

// 错误用法一:简单计算不需要 useMemo,引入的开销可能比计算本身还大
const doubled = useMemo(() => count * 2, [count]); // 普通乘法不需要缓存

// 错误用法二:对象字面量每次渲染都是新引用,无法有效缓存
const style = useMemo(() => ({ color: 'red' }), []); // 实际用处不大

// 错误用法三:在 useMemo/useCallback 内部的函数如果依赖频繁变化的对象,
// 缓存效果极差,不如直接写

---

五、常见问题(FAQ)

#### Q1:useCallback 和 useMemo 的区别是什么?

useCallback 缓存函数本身(返回稳定的函数引用),本质是 useMemo(() => fn, deps) 的语法糖;useMemo 缓存任意计算结果(数字、对象、JSX 等)。如果你要缓存的是回调函数,两者等价,但 useCallback 语义更清晰。

// 两者等价:
const handleClick = useCallback(() => { doSomething(a); }, [a]);
const handleClick = useMemo(() => () => { doSomething(a); }, [a]);

#### Q2:useMemo 的依赖数组可以为空吗?

可以。空依赖数组 [] 表示这个值永远不重新计算(永远返回第一次计算的结果)。但要注意:如果这个值引用了组件内部定义的对象或函数(它们每次渲染都是新引用),空依赖会导致"过期闭包"问题——useMemo 持有的是过时的引用。

// 正确:缓存一个不依赖任何渲染时变量的常量
const allCountries = useMemo(() => ['China', 'USA', 'Japan'], []);

// 危险:缓存了一个依赖外部变量的值,但声明依赖为空
const total = useMemo(() => price * discount, []); // price 变化时 total 不更新!

#### Q3:React.memo 和 useMemo 有什么关系?

React.memo 是一个高阶组件,用于包装子组件,让它在 props 没变时跳过渲染。useMemo 是一个 Hook,用于缓存计算结果或函数引用。两者常配合使用:父组件用 useCallback 稳定传给子组件的回调,子组件用 React.memo 避免无意义的渲染。

// 父组件:用 useCallback 稳定回调引用
const handleClick = useCallback(() => doSomething(id), [id]);

// 子组件:用 React.memo 跳过 props 未变化的渲染
const Button = React.memo(({ onClick, label }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>{label}</button>;
});

---

六、延伸阅读

  • [React Hooks 完全指南:useState 与 useEffect 核心用法]()
  • [React useRef 完全指南:操作 DOM 与持久化变量]()
  • [React 组件设计模式:高阶组件与 Render Props]()
  • [React 性能优化实战:Profiler、渲染分析与优化策略]()

---