React 性能优化的“卧龙凤雏”:useMemo 与 useCallback 到底该怎么用

在 React 的世界里,组件的渲染就像一场"牵一发而动全身"的多米诺骨牌。父组件打个喷嚏(State 变了),底下的子组件全得跟着感冒(重新渲染)。

虽然 React 够快,但如果你的组件里住着一只"吞金兽"(昂贵的计算逻辑),或者你的子组件是个"强迫症"(非要 Props 完全没变才肯不渲染),那你就得请出 React 性能优化的两尊大神了:useMemouseCallback

很多人分不清它俩,其实很简单:

  • useMemo 缓存的是结果(脑子转完产出的东西)。
  • useCallback 缓存的是函数本身(干活的工具)。

今天咱们就拿一段真实的代码,扒一扒这俩货到底怎么帮我们省资源。


useMemo:给你的组件装个"缓存大脑"

想象一下,你有一个超级复杂的数学题要算(比如从 0 加到 1000 万)。

优化前:笨笨的复读机

看这段代码,我们有一个 slowSum 函数,它模拟了一个耗时的计算过程:

JavaScript

javascript 复制代码
// 昂贵的计算:模拟 CPU 密集型任务
function slowSum(n) {
  console.log('🔥 疯狂计算中...');
  let sum = 0;
  // 假装这里跑了很久
  for (let i = 0; i < n * 10000000; i++) {
    sum += i;
  }
  return sum;
}

export default function App() {
  const [count, setCount] = useState(0); // 这个 state 和计算毫无关系
  const [num, setNum] = useState(1);     // 这个 state 才是计算需要的

  // 😱 灾难现场:
  // 只要组件重新渲染(比如你点击了 count+1),这行代码就会重新跑一遍!
  const result = slowSum(num); 

  return (
    <>
      <p>计算结果:{result}</p>
      {/* 点击这里,slowSum 居然也会执行?! */}
      <button onClick={() => setCount(count + 1)}>Count + 1 (无辜路人)</button> 
      <button onClick={() => setNum(num + 1)}>Num + 1 (正主)</button>
    </>
  )
}

痛点 :当你点击 Count + 1 时,明明 num 没变,结果也没变,但 React 重新执行组件函数,slowSum 又傻乎乎地跑了一遍。页面卡顿随之而来。

优化后:学会"偷懒"

这时候 useMemo 就登场了。它像一个记性很好的会计,只有当依赖项(账本)变了,它才重新算。

JavaScript

scss 复制代码
// ✅ 智能缓存
const result = useMemo(() => {
  return slowSum(num);
}, [num]); // 👈 只有当 num 变了,才重新跑里面的函数

现在你再疯狂点击 Count + 1,控制台不会再打印"计算中...",页面丝般顺滑。

场景二:代替 Vue 的 Computed

除了昂贵计算,useMemo 也是处理派生状态 的神器,类似于 Vue 里的 computed

比如这里有一个过滤列表的需求:

JavaScript

ini 复制代码
const [keyword, setKeyword] = useState('');
const list = ['apple', 'banana', 'orange', 'pear'];

// 如果不用 useMemo:
// 每次组件渲染(比如 count 变了),filter 都会重新遍历数组。
// 虽然这里数组小看不出性能损耗,但如果是大数据列表,这就是性能杀手。

const filterList = useMemo(() => {
  // 只有关键词变了,我才重新过滤
  return list.filter(item => item.includes(keyword));
}, [keyword]);

(注:includes('') 默认为 true,所以初始状态会显示所有水果,完美符合搜索逻辑。)


useCallback + memo:父子组件的"定情信物"

接下来聊聊 useCallback。很多人觉得:"我不就传个函数给子组件吗,为啥要包一层?"

这得从 JavaScript 的特性说起。

优化前:最熟悉的陌生人

父组件给子组件传 Props,子组件用 React.memo 包裹,本来是想做性能优化(Props 不变就不重新渲染)。但是...

JavaScript

javascript 复制代码
// 子组件:使用了 memo,理论上 Props 不变我就不渲染
const Child = memo(({ handleClick }) => {
  console.log('👶 Child 重新渲染了 (我不想这样)');
  return <div onClick={handleClick}>子组件</div>
});

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

  // 😱 问题出在这里:
  // 每次 App 重新渲染,handleClick 都会被重新定义!
  // 在 JS 里,function A() {} !== function A() {}
  // 引用地址变了 -> memo 认为 Props 变了 -> 子组件被迫渲染
  const handleClick = () => {
    console.log('click');
  }

  return (
    <div>
      {/* 我改了 count,跟 Child 半毛钱关系没有,但 Child 还是渲染了 */}
      <button onClick={() => setCount(count + 1)}>Count + 1</button>
      <Child handleClick={handleClick} />
    </div>
  )
}

痛点React.memo 就像一个严格的保安,它对比 Props 是否变化用的是"浅比较"(引用对比)。因为父组件每次渲染都生成一个新的函数地址,保安觉得:"这函数换人了!" 于是放行,导致子组件无意义渲染。

优化后:给函数发个"身份证"

useCallback 的作用就是把这个函数"固化"下来。

JavaScript

javascript 复制代码
// ✅ 保持函数引用稳定
const handleClick = useCallback(() => {
  console.log('click');
}, []); // 依赖为空,说明这个函数永远是同一个引用地址

现在,当你点击 Count + 1 时,父组件重渲染了,但 handleClick 还是原来那个 handleClickChild 组件发现 Props 没变,就安心地躺平不渲染了。

注意 :如果你需要在回调里用到 count,记得把它加进依赖数组:

JavaScript

javascript 复制代码
const handleClick = useCallback(() => {
  // 如果依赖数组里没写 count,这里永远打印 0 (闭包陷阱)
  console.log('click', count); 
}, [count]); 
// 👆 一旦 count 变了,函数引用还是会变,Child 还是会渲染。
// 这是为了保证逻辑正确性必须付出的代价。

总结

别为了优化而优化。useMemouseCallback 也是有成本的(它们本身也需要消耗内存来做依赖对比)。

请遵循这套"心法":

  1. useMemo

    • 昂贵计算 :当你看到 for 循环次数巨大,或者复杂的递归时。
    • 引用稳定 :当你计算出的对象/数组,要作为 useEffect 的依赖项,或者传给被 memo 包裹的子组件时。
  2. useCallback

    • 配合 React.memo :当你的函数需要传给一个"很重"的子组件,且该子组件被 memo 包裹时。
    • 作为 Hooks 依赖 :当这个函数要被用作 useEffect 的依赖项时。
相关推荐
aPurpleBerry9 分钟前
React 01 目录结构、tsx 语法
前端·react.js
basestone3 小时前
🚀 从重复 CRUD 到工程化封装:我是如何设计 useTableList 统一列表逻辑的
javascript·react.js·ant design
IT=>小脑虎5 小时前
2026版 React 零基础小白进阶知识点【衔接基础·企业级实战】
前端·react.js·前端框架
IT=>小脑虎5 小时前
2026版 React 零基础小白入门知识点【基础完整版】
前端·react.js·前端框架
骑自行车的码农6 小时前
🕹️ 设计一个 React 重试
前端·算法·react.js
黎明初时8 小时前
React基础框架搭建8-axios封装与未封装,实现 API 请求管理:react+router+redux+axios+Tailwind+webpack
javascript·react.js·webpack
OEC小胖胖9 小时前
03|从 `ensureRootIsScheduled` 到 `commitRoot`:React 工作循环(WorkLoop)全景
前端·react.js·前端框架
weibkreuz9 小时前
函数柯里化@11
前端·javascript·react.js
用户9824505141810 小时前
react中useState、useEffect、useCallback、useMemo 的区别与使用场景。
react.js
chao_66666611 小时前
React Native + Expo 开发指南:编译、调试、构建全解析
javascript·react native·react.js