在 React 开发中,性能优化是提升用户体验和应用响应速度的关键环节。虽然 React 本身已经做了大量优化工作,但在复杂场景下,我们仍需借助一些“利器”来避免不必要的渲染和计算。本文将深入浅出地讲解 React 中三个核心性能优化工具:useMemo、useCallback 和 memo,并通过常见误区、面试题和答疑环节帮助你真正掌握它们的使用时机与原理。
🔍 一、为什么需要性能优化?
React 的核心思想是“状态驱动 UI”。每当状态(state)或属性(props)发生变化时,组件会重新执行函数体(对于函数组件),并可能触发子组件的重新渲染。
但问题来了:
- 并非所有状态变化都需要更新整个组件树。
- 某些计算(如大数据过滤、复杂数学运算)非常“昂贵”,重复执行会拖慢应用。
- 每次父组件重渲染,传递给子组件的函数都会生成新引用,导致子组件即使 props 内容没变也会被强制更新。
这就引出了我们的三位主角 👇
🧠 二、useMemo:缓存计算结果
✅ 是什么?
useMemo 用于缓存昂贵的计算结果,只有当依赖项发生变化时,才重新计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
💡 使用场景
- 过滤/排序大型列表
- 复杂数学计算(如斐波那契、大数求和)
- 创建不可变对象或数组(避免每次渲染都新建)
📌 示例说明
假设你有一个搜索框,用户输入关键词过滤水果列表:
const filterList = useMemo(() => {
return list.filter(item => item.includes(keyword));
}, [keyword]);
如果不用
useMemo,每次count改变(即使和搜索无关),filterList都会重新执行,浪费性能。
❗ 易错点 & 答疑
❓ Q1:useMemo 能完全阻止函数执行吗?
不能! 它只是缓存返回值。如果依赖项没变,函数体不会执行,直接返回上次缓存的结果。
❓ Q2:空字符串 "" 用 includes 会返回 true 吗?
✅ 是的!"apple".includes("") 返回 true。这在搜索清空时可能导致显示全部结果——这是预期行为,但初学者常误以为是 bug。
❓ Q3:依赖数组写错了会怎样?
- 漏写依赖:可能读到旧值,导致逻辑错误。
- 多写依赖:导致不必要的重新计算,失去优化意义。
✅ 建议:让 ESLint 的
react-hooks/exhaustive-deps规则帮你检查!
🎯 三、useCallback:缓存函数引用
✅ 是什么?
useCallback 用于缓存函数本身,确保在依赖不变时,函数引用保持一致。
const handleClick = useCallback(() => {
console.log('clicked');
}, [deps]);
等价于:
const handleClick = useMemo(() => () => { ... }, [deps]);
💡 使用场景
- 将回调函数传递给经过
memo优化的子组件 - 避免因函数引用变化导致子组件不必要重渲染
📌 示例说明
父组件传递 handleClick 给子组件:
const handleClick = useCallback(() => {
console.log('click', count);
}, [count]); // 依赖 count,因为函数内用了它
<Child count={count} handleClick={handleClick} />
若不用 useCallback,每次父组件重渲染都会生成新函数,即使 count 没变,子组件也会因 handleClick 引用变化而重渲染。
❗ 易错点 & 答疑
❓ Q1:所有函数都要用 useCallback 包裹吗?
不是! 只有在以下情况才需要:
- 函数作为 prop 传给
memo子组件 - 函数被用作
useEffect、useMemo等 Hook 的依赖
⚠️ 盲目使用反而增加内存开销和代码复杂度!
❓ Q2:依赖项里要不要加函数?
如果函数是组件内定义的(非 useCallback 缓存),它每次都是新的,必须加;但如果已经是 useCallback 缓存过的,且依赖正确,可不加。
❓ Q3:面试题:useCallback(fn, []) 和 useRef 存函数有什么区别?
useCallback(fn, []):返回一个稳定函数,但闭包锁定初始状态(可能读不到最新 state)。useRef存函数:可手动更新,总能访问最新状态,但需自行管理。
✅ 推荐优先用
useCallback+ 正确依赖,更符合 React 响应式理念。
🛡️ 四、memo:跳过子组件重渲染
✅ 是什么?
memo 是一个高阶组件(HOC) ,用于包裹函数组件,实现类似 class 组件 shouldComponentUpdate 的功能。
const Child = memo(({ count, handleClick }) => {
return <div>{count}</div>;
});
💡 工作原理
- 默认对 props 进行浅比较(shallow compare)
- 如果 props 引用没变 → 跳过渲染,复用上次结果
📌 关键前提
-
传递给
memo组件的 props 必须是稳定的引用- 对象/数组:用
useMemo缓存 - 函数:用
useCallback缓存
- 对象/数组:用
否则 memo 会失效!
❗ 易错点 & 答疑
❓ Q1:memo 能优化所有子组件吗?
不能! 如果子组件接收的 props 每次都是新对象/新函数,memo 无法生效。
✅ 优化原则:父稳子才稳
❓ Q2:对象属性变了但引用没变,memo 能检测到吗?
不能! 浅比较只看引用。如果你 mutate(修改原对象),React 无法感知变化。
✅ 必须用不可变更新(如展开运算符
{...obj})才能触发重渲染。
❓ Q3:什么时候不该用 memo?
- 组件很简单(渲染成本低)
- props 总是变化(如每帧动画)
- 组件内部有
useState/useContext且频繁更新
📊 性能优化前先测量!不要过早优化。
🧩 五、三者关系总结
| 工具 | 作用 | 缓存内容 | 典型场景 |
|---|---|---|---|
useMemo | 缓存计算结果 | 值(如数组、数字、对象) | 过滤列表、复杂计算 |
useCallback | 缓存函数引用 | 函数本身 | 传给 memo 子组件的回调 |
memo | 跳过子组件渲染 | 整个组件输出 | 展示型子组件,props 稳定 |
💡 它们常常配合使用:
useCallback→ 保证函数稳定 → 传给memo子组件 → 避免重渲染
useMemo→ 保证数据稳定 → 作为 props 传给子组件
🎓 面试题精选
📌 Q:React 中如何避免子组件不必要的重渲染?
A:
- 用
memo包裹子组件 - 父组件用
useCallback缓存传给子组件的函数 - 用
useMemo缓存传给子组件的对象/数组 - 确保 props 使用不可变更新
📌 Q:useMemo 和 useCallback 本质区别是什么?
A:
useMemo缓存的是函数执行后的返回值useCallback缓存的是函数本身(相当于useMemo(() => fn, deps))
✅ 结语
useMemo、useCallback 和 memo 是 React 性能优化的“黄金三角”。但请记住:
🔥 优化是手段,不是目的。先写出清晰正确的代码,再根据性能瓶颈针对性优化。
盲目使用这些 Hook 反而会增加代码复杂度和潜在 bug。善用 React DevTools 的「Highlight Updates」功能,观察哪些组件在不必要地重渲染,再决定是否引入优化。
希望这篇详解能帮你彻底掌握这三个核心概念!如有疑问,欢迎在评论区讨论 💬
✨ Happy Coding with React! ✨