Hook React 18+

useMemo & useCallback:性能优化

学习如何使用 useMemo 和 useCallback 优化 React 组件性能

为什么要性能优化?

React 默认会在组件的状态或 Props 改变时重新渲染组件。在某些情况下,这可能导致不必要的计算或渲染。

常见性能问题

  • 昂贵的计算在每次渲染时都重新执行
  • 子组件因为父组件重新渲染而不必要地重新渲染
  • 函数引用在每次渲染时都改变,导致子组件失去优化
重要:优化原则

性能优化的三个原则

  1. 先测量,后优化:使用 React DevTools Profiler 识别真正的性能瓶颈
  2. 避免过早优化:简单计算(如加减乘除、字符串拼接)不需要 useMemo
  3. 优化有成本:useMemo 和 useCallback 本身也有开销,只在确实需要时使用

判断标准:如果一个计算的执行时间超过 1ms,或者组件每秒渲染超过 60 次, 才考虑使用 useMemo 优化。

何时不需要优化

TypeScript
// ✅ 这些情况不需要 useMemo
function Component({ a, b }) {
  // 简单数学运算(< 0.001ms)
  const sum = a + b;

  // 简单字符串操作(< 0.001ms)
  const greeting = `Hello, ${name}`;

  // 简单数组映射(小数组,< 1ms)
  const doubled = numbers.map(n => n * 2);

  return <div>{sum}</div>;
}

// ❌ 不要这样优化(开销大于收益)
function Component({ a, b }) {
  const sum = useMemo(() => a + b, [a, b]); // 不必要!
  return <div>{sum}</div>;
}

useMemo:缓存计算结果

useMemo 缓存计算结果,只有在依赖项改变时才重新计算。 这就好比你把计算结果记在笔记本上,下次如果输入没变,直接查笔记本就行,不用重新算一遍。

基本语法

TypeScript
import { useMemo } from 'react';

const cachedValue = useMemo(calculateValue, dependencies);
  • calculateValue: 计算缓存值的函数
  • dependencies: 依赖数组,只有这些值改变时才重新计算

示例:耗费性能的过滤操作

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

function ProductList({ products }) {
  const [filter, setFilter] = useState('');

  // ❌ 不使用 useMemo:每次渲染都重新过滤
  // const filteredProducts = products.filter(p =>
  //   p.name.toLowerCase().includes(filter.toLowerCase())
  // );

  // ✅ 使用 useMemo:只在 products 或 filter 改变时重新计算
  const filteredProducts = useMemo(() => {
    console.log('过滤产品...');
    return products.filter(p =>
      p.name.toLowerCase().includes(filter.toLowerCase())
    );
  }, [products, filter]);

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="搜索产品..."
      />
      <ul>
        {filteredProducts.map(product => (
          <li key={product.id}>{product.name}</li>
        ))}
      </ul>
    </div>
  );
}

示例:复杂计算

TypeScript
function FibonacciCalculator({ n }) {
  // ✅ 缓存斐波那契数列计算结果
  const result = useMemo(() => {
    console.log(`计算斐波那契数列第 ${n} 项`);
    function fib(n) {
      if (n <= 1) return n;
      return fib(n - 1) + fib(n - 2);
    }
    return fib(n);
  }, [n]);

  return <div>斐波那契数列第 {n} 项: {result}</div>;
}
何时使用 useMemo?
  • 计算成本高(如复杂的数据转换)
  • 依赖项很少改变
  • 计算结果用于渲染或其他 Hook

useCallback:缓存函数引用

useCallback 缓存函数定义,只有在依赖项改变时才返回新的函数引用。

基本语法

TypeScript
import { useCallback } from 'react';

const cachedFn = useCallback(fn, dependencies);
  • fn: 要缓存的函数
  • dependencies: 依赖数组

useCallback vs useMemo

TypeScript
// useCallback 实际上是 useMemo 的语法糖

// 使用 useCallback
const memoizedCallback = useCallback(() => {
  doSomething(a, b);
}, [a, b]);

// 等价于使用 useMemo
const memoizedCallback = useMemo(() => {
  return () => doSomething(a, b);
}, [a, b]);

示例:传递给优化的子组件

TypeScript
import { useCallback, memo, useState } from 'react';

// 子组件使用 memo 优化
const Button = memo(function Button({ onClick, children }) {
  console.log('Button 渲染');
  return <button onClick={onClick}>{children}</button>;
});

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

  // ❌ 不使用 useCallback:每次渲染都创建新函数
  // const handleClick = () => {
  //   console.log('点击了!');
  // };

  // ✅ 使用 useCallback:函数引用稳定
  const handleClick = useCallback(() => {
    console.log('点击了!');
  }, []); // 空依赖数组,函数永不改变

  return (
    <div>
      <Button onClick={handleClick}>点击我</Button>
      <button onClick={() => setCount(count + 1)}>
        计数: {count}
      </button>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
    </div>
  );
}
为什么需要稳定的函数引用?

如果子组件使用 React.memo 优化,但父组件每次渲染都传递新的函数引用,子组件还是会重新渲染。使用 useCallback 可以保持函数引用稳定。

示例:带依赖的 useCallback

TypeScript
function ProductList({ products }) {
  const [cart, setCart] = useState([]);

  // ✅ 只在 products 改变时重新创建函数
  const addToCart = useCallback((productId) => {
    const product = products.find(p => p.id === productId);
    setCart(prev => [...prev, product]);
  }, [products]);

  return (
    <div>
      {products.map(product => (
        <ProductItem
          key={product.id}
          product={product}
          onAddToCart={addToCart}
        />
      ))}
    </div>
  );
}

const ProductItem = memo(function ProductItem({ product, onAddToCart }) {
  return (
    <div>
      <span>{product.name}</span>
      <button onClick={() => onAddToCart(product.id)}>
        添加到购物车
      </button>
    </div>
  );
});

React.memo:跳过不必要的渲染

React.memo 是一个高阶组件,如果 Props 没有改变,就跳过组件的渲染。

基本用法

TypeScript
import { memo } from 'react';

const MyComponent = memo(function MyComponent(props) {
  // 只有在 Props 改变时才重新渲染
  return <div>{props.name}</div>;
});

示例:优化列表项

TypeScript
import { memo } from 'react';

// 使用 memo 优化列表项组件
const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }) {
  console.log(`TodoItem ${todo.id} 渲染`);

  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>删除</button>
    </li>
  );
});

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: '学习 React', completed: false },
    { id: 2, text: '学习 TypeScript', completed: false },
  ]);

  // 使用 useCallback 稳定函数引用
  const handleToggle = useCallback((id) => {
    setTodos(prev =>
      prev.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);

  const handleDelete = useCallback((id) => {
    setTodos(prev => prev.filter(todo => todo.id !== id));
  }, []);

  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </ul>
  );
}
React.memo 的浅比较

React.memo 默认使用浅比较来检查 Props。如果需要自定义比较逻辑,可以传递比较函数作为第二个参数。

TypeScript
const MyComponent = memo(
  function MyComponent(props) {
    return <div>{props.name}</div>;
  },
  (prevProps, nextProps) => {
    // 返回 true 表示 Props 相等,不需要重新渲染
    return prevProps.name === nextProps.name;
  }
);

使用场景

场景 1:昂贵的计算

TypeScript
function DataTable({ data, sortBy }) {
  // ✅ 缓存排序后的数据
  const sortedData = useMemo(() => {
    return data.sort((a, b) => {
      if (a[sortBy] < b[sortBy]) return -1;
      if (a[sortBy] > b[sortBy]) return 1;
      return 0;
    });
  }, [data, sortBy]);

  return <table>...</table>;
}

场景 2:防止子组件重新渲染

TypeScript
function Parent() {
  const [count, setCount] = useState(0);

  // ✅ 稳定的函数引用
  const handleClick = useCallback(() => {
    console.log('点击');
  }, []);

  return (
    <>
      <button onClick={() => setCount(count + 1)}>
        计数: {count}
      </button>
      <ExpensiveChild onClick={handleClick} />
    </>
  );
}

const ExpensiveChild = memo(function ExpensiveChild({ onClick }) {
  console.log('ExpensiveChild 渲染');
  return <button onClick={onClick}>子组件</button>;
});

场景 3:作为其他 Hook 的依赖

TypeScript
function Chat({ roomId }) {
  const [message, setMessage] = useState('');

  // ✅ 使用 useCallback 确保 messageHandler 引用稳定
  const messageHandler = useCallback((msg) => {
    console.log('收到消息:', msg);
  }, []);

  useEffect(() => {
    const connection = connectToChat(roomId, messageHandler);
    return () => connection.disconnect();
  }, [roomId, messageHandler]);

  return <input value={message} onChange={(e) => setMessage(e.target.value)} />;
}

常见错误

1. 过度优化

TypeScript
// ❌ 差:简单计算不需要 useMemo
function Component({ a, b }) {
  const result = useMemo(() => a + b, [a, b]);
  return <div>{result}</div>;
}

// ✅ 好:直接计算
function Component({ a, b }) {
  return <div>{a + b}</div>;
}

2. 忘记依赖

TypeScript
// ❌ 错误:缺少依赖
const filtered = useCallback(() => {
  return items.filter(item => item.active);
}, []); // 应该包含 items

// ✅ 正确:包含所有依赖
const filtered = useCallback(() => {
  return items.filter(item => item.active);
}, [items]);

3. 在 useMemo 中执行副作用

TypeScript
// ❌ 错误:在 useMemo 中执行副作用
const value = useMemo(() => {
  localStorage.setItem('key', JSON.stringify(data)); // 副作用!
  return data;
}, [data]);

// ✅ 正确:在 useEffect 中执行副作用
useEffect(() => {
  localStorage.setItem('key', JSON.stringify(data));
}, [data]);

4. 误认为 useCallback 总是提升性能

TypeScript
// ❌ 差:没有必要的 useCallback
function Form() {
  const [value, setValue] = useState('');

  const handleChange = useCallback((e) => {
    setValue(e.target.value);
  }, []); // 没有意义,因为没有子组件使用这个函数

  return <input value={value} onChange={handleChange} />;
}

// ✅ 好:直接定义函数
function Form() {
  const [value, setValue] = useState('');

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  return <input value={value} onChange={handleChange} />;
}

性能测量

在优化之前,应该先测量性能以确定真正的瓶颈。

使用 React DevTools Profiler

TypeScript
import { Profiler } from 'react';

function onRenderCallback(
  id,
  phase,
  actualDuration,
  baseDuration,
  startTime,
  commitTime,
  interactions
) {
  console.log({
    id,
    phase,
    actualDuration,
    baseDuration,
  });
}

function App() {
  return (
    <Profiler id="App" onRender={onRenderCallback}>
      <Navigation />
      <Main />
    </Profiler>
  );
}

使用 performance.mark

TypeScript
function ExpensiveComponent({ data }) {
  performance.mark('expensive-start');

  const result = useMemo(() => {
    // 耗时计算
    return processData(data);
  }, [data]);

  performance.mark('expensive-end');
  performance.measure(
    'expensive-calculation',
    'expensive-start',
    'expensive-end'
  );

  return <div>{result}</div>;
}

最佳实践

  1. 先测量,再优化

使用 React DevTools Profiler 确定真正的性能瓶颈

  1. 只优化必要的部分

不要对所有组件和函数都使用 useMemo/useCallback

  1. 保持依赖数组准确

依赖数组必须包含所有外部值,否则可能产生过期的闭包

  1. 避免在 useMemo 中执行副作用

副作用应该在 useEffect 中执行,而不是在 useMemo 中

  1. 考虑使用 useReducer 减少依赖

如果依赖数组很长,考虑使用 useReducer 重构

TypeScript
// ❌ 很多依赖
const handleSomething = useCallback(() => {
  doSomething(a, b, c, d, e, f);
}, [a, b, c, d, e, f]);

// ✅ 使用 useReducer 减少
const [state, dispatch] = useReducer(reducer, initialState);

const handleSomething = useCallback(() => {
  dispatch({ type: 'DO_SOMETHING' });
}, []); // 无依赖!

下一步

现在你已经了解了性能优化,可以继续学习:

这篇文章有帮助吗?