React 性能优化利器:`useMemo` 与 `useCallback` 的正确使用姿势

36 阅读4分钟

在 React 开发中,随着应用复杂度的提升,性能问题逐渐显现。频繁的组件重渲染、重复的昂贵计算、不必要的函数重建……这些问题虽小,却可能成为拖慢用户体验的“隐形杀手”。

幸运的是,React 提供了两个强大的 Hook —— useMemo 和 useCallback,它们是函数组件中进行细粒度性能优化的核心工具。本文将带你深入理解它们的原理、适用场景与常见误区,并通过真实代码示例掌握最佳实践。


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

React 的核心理念是  “状态驱动视图” 。当状态(state)或属性(props)发生变化时,组件会重新执行其函数体,生成新的 UI。

但这带来一个问题:

即使某些数据未变,整个组件也会重新运行,导致不必要的计算和子组件重渲染。

例如:

  • 一个搜索框输入关键词,只应过滤列表,而不应影响其他无关状态(如计数器)
  • 一个子组件只依赖某个特定 prop,但父组件其他状态变化却导致它被重新渲染

这就是 useMemo 和 useCallback 大显身手的地方。


二、useMemo:缓存计算结果

✅ 作用

避免在每次渲染时重复执行昂贵的计算逻辑,仅在依赖项变化时才重新计算。

📌 语法

js

编辑

const memoizedValue = useMemo(() => {
  // 执行计算并返回结果
}, [dep1, dep2]);

🎯 典型场景

  1. 过滤/映射大型数组
  2. 复杂数学运算
  3. 派生状态(类似 Vue 的 computed

🔧 示例:避免无效的列表过滤

jsx

预览

const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange'];

// ❌ 每次组件渲染都会执行 filter(包括 count 变化时!)
// const filterList = list.filter(item => item.includes(keyword));

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

💡 注意:""(空字符串)调用 includes("") 会返回 true,这是 JavaScript 的正常行为,需根据业务判断是否需要处理。

⚠️ 警告:不要滥用

  • 对于简单计算(如 a + b),useMemo 的开销可能大于收益
  • 只用于真正“昂贵”的操作

三、useCallback:缓存函数引用

✅ 作用

防止函数在每次渲染时被重新创建,保持函数引用稳定,从而避免破坏子组件的 memo 优化。

📌 语法

js

编辑

const memoizedFn = useCallback(() => {
  // 函数逻辑
}, [dep1, dep2]);

🎯 典型场景

  • 将函数作为 prop 传递给 memo 包裹的子组件
  • 作为 useEffectuseMemo 的依赖项

🔧 示例:配合 memo 避免子组件无效重渲染

jsx

预览

// 子组件使用 memo 优化
const Child = memo(({ count, handleClick }) => {
  console.log('Child 渲染'); // 我们希望它只在 count 或 handleClick 逻辑变化时才打印
  return <div onClick={handleClick}>子组件 {count}</div>;
});

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

  // ❌ 每次渲染都创建新函数 → Child 总是重渲染
  // const handleClick = () => { console.log('click'); };

  // ✅ 缓存函数,仅当 count 变化时更新(若函数依赖 count)
  const handleClick = useCallback(() => {
    console.log('click', count); // 如果用到 count,必须加入依赖
  }, [count]);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      <button onClick={() => setNum(num + 1)}>num + 1</button> {/* 不影响 Child */}
      <Child count={count} handleClick={handleClick} />
    </div>
  );
}

✅ 效果:点击 “num + 1” 时,Child 不会重新渲染,因为它的 props(count 和 handleClick)均未变化。


四、useMemo vs useCallback:本质关系

其实,useCallback 是 useMemo 的语法糖:

js

编辑

// 这两行等价
const fn = useCallback(callback, deps);
const fn = useMemo(() => callback, deps);
  • useMemo 缓存 任意值
  • useCallback 专门缓存 函数

五、常见误区与最佳实践

❌ 误区 1:所有函数都包 useCallback

不需要!  只有当函数作为 prop 传给 memo 组件,或作为其他 Hook 的依赖时,才有必要。

❌ 误区 2:遗漏依赖项

js

编辑

const handleClick = useCallback(() => {
  console.log(count); // 用到了 count
}, []); // ❌ 忘记依赖 count → 闭包捕获旧值!

✅ 正确做法:把所有用到的响应式变量加入依赖数组

❌ 误区 3:过度优化

过早优化是万恶之源。先写出清晰的代码,再用 React DevTools 分析性能瓶颈,最后针对性优化。

✅ 最佳实践口诀:

“用到就加依赖,不用不加;昂贵才缓存,简单别乱搞。”


六、总结

表格

Hook用途核心价值
useMemo缓存计算结果避免重复执行昂贵计算
useCallback缓存函数引用配合 memo 防止子组件无效重渲染

它们共同服务于 React 的性能优化哲学:

“只在必要时更新,其余时间保持静默。”

掌握 useMemo 和 useCallback,不仅能写出更高效的 React 应用,更能深入理解 React 的响应式机制与闭包陷阱。但请记住:工具再好,也要用在刀刃上。


🌟 延伸思考
如果你的组件中有多个派生状态或复杂逻辑,是否考虑将部分逻辑抽离为自定义 Hook?这能让代码更可维护,也更容易复用优化策略。

希望这篇文章能帮你真正掌握这两个 Hook!