React 性能优化双子星:useMemo 与 useCallback 完全指南

28 阅读4分钟

一、为什么需要性能优化?

React 的核心机制是:状态变化 → 组件函数重新执行 → 生成新的 UI

但问题在于:

  • 每次重渲染,所有变量、函数都会重新创建

  • 即使某些数据或逻辑并未改变,也会被重复执行

  • 若涉及昂贵计算传递给子组件的函数/对象,会导致:

    • CPU 浪费(如循环百万次)
    • 子组件无谓重渲染(因 props 引用变化)

🎯 目标:只在真正需要时才重新计算或渲染。


二、useMemo:缓存计算结果

✅ 核心作用

缓存函数的返回值,避免重复执行昂贵计算。

示例解析

jsx

预览

// 昂贵计算:循环 n * 100 万次
function slowSum(n) {
  let sum = 0;
  for (let i = 0; i < n * 1000000; i++) sum += i;
  return sum;
}

export default function App() {
  const [count, setCount] = useState(0);
  const [keyword, setKeyword] = useState('');
  const [num, setNum] = useState(0);
  const list = ['apple', 'banana', 'orange', 'pear'];

  // ✅ 仅当 keyword 变化时才重新过滤
  const filterList = useMemo(() => {
    return list.filter(item => item.includes(keyword));
  }, [keyword]);

  // ✅ 仅当 num 变化时才重新计算 slowSum
  const result = useMemo(() => slowSum(num), [num]);

  return (
    <div>
      <p>结果:{result}</p>
      <button onClick={() => setNum(num + 1)}>num + 1</button>
      <input value={keyword} onChange={e => setKeyword(e.target.value)} />
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      {filterList.map(item => <li key={item}>{item}</li>)}
    </div>
  );
}

关键点说明:

表格

问题未优化表现使用 useMemo 后
点击 “count + 1”filterList 和 slowSum 都会重新执行完全跳过,因依赖项未变
输入关键词仅 filterList 重新计算✅ 符合预期
修改 num仅 slowSum 重新计算✅ 符合预期

💡 注意"".includes("") 返回 true,所以空输入会显示全部列表——这是 JS 行为,非 bug。


三、useCallback:缓存函数引用

✅ 核心作用

缓存函数本身,避免每次渲染都创建新函数,从而防止子组件因 props 引用变化而重渲染。

场景:配合 React.memo 优化子组件

jsx

预览

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

// ✅ 用 memo 包裹子组件:仅当 props 引用变化时才重渲染
const Child = memo(({ count, handleClick }) => {
  console.log('Child 重新渲染');
  return <div onClick={handleClick}>子组件 {count}</div>;
});

export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  // ❌ 普通写法:每次父组件重渲染,handleClick 都是新函数
  // const handleClick = () => console.log('click');

  // ✅ 用 useCallback 缓存函数
  const handleClick = useCallback(() => {
    console.log('click');
  }, []); // 依赖为空,函数永不更新

  return (
    <div>
      <p>count: {count}</p>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      
      <p>num: {num}</p>
      <button onClick={() => setNum(num + 1)}>num + 1</button>

      {/* 传递缓存后的函数 */}
      <Child count={count} handleClick={handleClick} />
    </div>
  );
}

行为对比:

表格

操作未用 useCallback使用 useCallback
点击 “num + 1”Child 重渲染(因 handleClick 是新函数)Child 不重渲染handleClick 引用不变)
点击 “count + 1”Child 重渲染(因 count 变)Child 重渲染(合理,因 count 确实变了)

📌 关键思想:React 数据流 = 父管数据,子管展示。通过 memo + useCallback 实现“按需更新”。


四、useMemo vs useCallback:一张表说清区别

表格

特性useMemouseCallback
用途缓存(计算结果)缓存函数
等价写法useMemo(() => compute(), deps)useMemo(() => fn, deps)
典型场景过滤列表、格式化数据、复杂数学运算传递给子组件的事件回调
返回类型任意值(数字、数组、对象等)函数

✅ 记住:useCallback(fn, deps) ≈ useMemo(() => fn, deps)


五、注意事项 & 常见误区

1. 不要为了优化而优化

  • 对简单操作(如 a + b)使用 useMemo 反而增加内存开销。
  • 先测量性能瓶颈(用 React DevTools Profiler),再决定是否优化。

2. 依赖项必须完整

js

编辑

// ❌ 危险:list 变了也不会更新 filterList
const filterList = useMemo(() => list.filter(...), []);

// ✅ 正确
const filterList = useMemo(() => list.filter(...), [list, keyword]);

3. 函数依赖状态时,需谨慎

js

编辑

const handleClick = useCallback(() => {
  console.log(count); // 依赖 count
}, [count]); // 必须加入依赖!

否则会捕获旧的闭包值(stale closure)。

4. useMemo 不能替代 useEffect

  • useMemo 用于同步计算,不应用于副作用(如发请求、订阅)。
  • 副作用请用 useEffect

六、总结要点(方便复习)

✅ useMemo 适用场景

  • 过滤/映射大型数组
  • 昂贵计算(如 slowSum
  • 派生状态(如格式化日期、计算总额)

✅ useCallback 适用场景

  • 将回调函数传递给 React.memo 包裹的子组件
  • 避免因函数引用变化导致子组件重渲染

✅ 黄金法则

只缓存真正昂贵或影响渲染的值/函数

✅ 组合拳

js

编辑

父组件:useCallback 缓存函数 + useMemo 缓存数据  
子组件:React.memo 阻止无谓渲染

七、拓展思考

Q:Vue 的 computed 和 React 的 useMemo 有何异同?

  • 相同:都用于派生状态的缓存。

  • 不同

    • Vue 自动追踪依赖,无需手动声明;
    • React 显式声明依赖,更灵活但需开发者负责正确性。

Q:能否用 useRef 替代 useMemo?

  • useRef 可存储值且不触发重渲染,但不会自动响应依赖变化
  • useMemo 更语义化、自动响应依赖,优先使用 useMemo