《React 性能优化:useMemo 与 useCallback 实战》

0 阅读9分钟

React 性能优化必看:useMemo 与 useCallback 实战解析(附完整代码)

作为 React 开发者,你是否遇到过这样的问题:组件明明只改了一个无关状态,却触发了不必要的重新渲染、昂贵的计算重复执行,导致页面卡顿?

其实这不是 React 的“bug”,而是函数组件的默认行为——只要组件的状态(state)或属性(props)发生改变,整个组件函数就会重新执行一遍

而 useMemo 和 useCallback,就是 React 官方提供的两个“性能优化利器”,专门解决这类问题。今天结合具体代码案例,从“痛点→解决方案→实战用法”,带你彻底搞懂这两个 Hook 的用法,再也不用为组件性能焦虑!

一、先搞懂:为什么需要 useMemo 和 useCallback?

在讲用法之前,我们先明确核心痛点——不必要的计算和不必要的组件重渲染,这也是我们优化的核心目标。

痛点1:无关状态改变,触发昂贵计算重复执行

先看一段未优化的代码(简化版):

import { useState } from 'react';

// 模拟昂贵的计算(比如大数据量处理、复杂运算)
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); 
  const [num, setNum] = useState(0);
  
  // 昂贵的计算,依赖 num
  const result = slowSum(num);

  return (
    计算结果:{result}<button onClick={ setNum(num + 1)}>num+1(触发计算)无关状态:{count}<button onClick={ setCount(count + 1)}>count+1(无关操作)
  );
}

运行后你会发现:点击「count+1」(改变和计算无关的状态),控制台依然会打印「计算中...」——这意味着,即使计算依赖的 num 没有变,昂贵的 slowSum 函数也会重新执行

这就是典型的“无效计算”,当计算足够复杂时,会明显拖慢页面性能。

痛点2:无关状态改变,触发子组件重复渲染

React 中,父组件重新渲染时,默认会带动所有子组件一起重新渲染。即使子组件的 props 没有任何变化,也会“无辜躺枪”。

再看一段未优化的代码:

import { useState } from 'react';

// 子组件:仅展示 count 和触发点击事件
const Child = ({ count, handleClick }) => {
  console.log('子组件重新渲染'); // 观察重渲染情况
  return (
    <div onClick={子组件:{count}
  );
};

export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  
  // 父组件传递给子组件的回调函数
  const handleClick = () => {
    console.log('点击子组件');
  };

  return (
    <button onClick={ setCount(count + 1)}>count+1(关联子组件)<button onClick={ setNum(num + 1)}>num+1(无关子组件)<Child count={count} handleClick={handleClick} />
    
  );
}

运行后发现:点击「num+1」(改变和子组件无关的状态),控制台依然会打印「子组件重新渲染」。

原因很简单:父组件重新执行时,会重新生成一个新的 handleClick 函数(即使函数逻辑没变),而子组件的 props 包含这个新函数,React 会认为“props 变了”,从而触发子组件重渲染。

而这两个痛点,正好可以用 useMemo 和 useCallback 分别解决——useMemo 缓存计算结果,useCallback 缓存回调函数。

二、useMemo:缓存计算结果,避免无效计算

1. 核心作用

useMemo(Memo = Memoization,记忆化)的核心功能是:缓存“昂贵计算”的结果,只有当依赖项发生改变时,才重新执行计算;依赖项不变时,直接返回缓存的结果

相当于 Vue 中的 computed 计算属性,专门用于处理“依赖某个/某些状态、需要重复执行的计算逻辑”。

2. 语法格式

const 缓存的结果 = useMemo(() => {
  // 这里写需要缓存的计算逻辑
  return 计算结果;
}, [依赖项数组]);

参数说明:

  • 第一个参数:函数,封装需要缓存的计算逻辑,函数的返回值就是要缓存的结果。
  • 第二个参数:依赖项数组,只有当数组中的依赖项发生改变时,才会重新执行第一个参数的函数,重新计算结果;否则直接返回缓存值。

3. 实战优化:解决“无效计算”问题

我们用 useMemo 优化前面的“昂贵计算”案例:

import { useState, useMemo } from 'react'; // 导入 useMemo

// 模拟昂贵的计算
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); 
  const [num, setNum] = useState(0);
  
  // 用 useMemo 缓存计算结果,依赖项只有 num
  const result = useMemo(() => {
    return slowSum(num); // 计算逻辑封装在函数中
  }, [num]); // 只有 num 改变时,才重新计算

  return (
计算结果:{result}<button onClick={ setNum(num + 1)}>num+1(触发计算)无关状态:{count}<button onClick={ setCount(count + 1)}>count+1(无关操作)
  );
}

优化后效果:

  • 点击「num+1」:num 改变,依赖项变化,重新执行 slowSum,打印「计算中...」;
  • 点击「count+1」:count 改变,但 num 未变,依赖项不变,直接返回缓存的 result,不再执行 slowSum,控制台无打印。

4. 补充案例:缓存列表过滤结果

除了昂贵计算,列表过滤、数据处理等场景也适合用 useMemo。比如下面的列表过滤案例:

import { useState, useMemo } from 'react';

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

  // 用 useMemo 缓存过滤结果,依赖项只有 keyword
  const filterList = useMemo(() => {
    console.log('过滤执行');
    return list.filter(item => item.includes(keyword));
  }, [keyword]); // 只有 keyword 改变时,才重新过滤

  return (
    <input 
        type="text" 
        value={ setKeyword(e.target.value)}
        placeholder="搜索水果"
      />
      无关状态:{count}<button onClick={ setCount(count + 1)}>count+1
        {filterList.map(item => (<li key={{item}
        ))}
      
  );
}

优化后:只有输入关键词(keyword 改变)时,才会重新执行过滤;点击 count+1 时,过滤逻辑不会重复执行,提升组件性能。

5. 注意点

  • 不要滥用 useMemo:如果计算逻辑很简单(比如 count * 2),使用 useMemo 反而会增加缓存的开销,得不偿失;
  • 依赖项数组不能漏:如果计算逻辑依赖某个状态,但没写进依赖数组,useMemo 会一直返回初始缓存值,导致数据不一致;
  • useMemo 缓存的是“计算结果”,不是函数本身。

三、useCallback:缓存回调函数,避免子组件无效重渲染

1. 核心作用

useCallback 的核心功能是:缓存回调函数本身,避免父组件重新渲染时,频繁生成新的函数实例,从而防止子组件因 props 变化而无效重渲染

它常和 memo(高阶组件)配合使用——memo 用于优化子组件,避免子组件在 props 未变时重渲染;useCallback 用于缓存传递给子组件的回调函数,确保函数实例不变。

2. 先认识 memo

在讲 useCallback 之前,必须先了解 memo:

  • memo 是 React 提供的高阶组件(HOC),接收一个函数组件作为参数,返回一个“优化后的新组件”;
  • 作用:对比子组件的前后 props,如果 props 没有变化,就阻止子组件重新渲染;
  • 局限性:只能浅对比 props(基本类型对比值,引用类型对比地址),如果传递的是函数、对象,memo 会认为“地址变了,props 变了”,依然会触发重渲染。

3. 语法格式

const 缓存的回调函数 = useCallback(() => {
  // 这里写回调函数的逻辑
}, [依赖项数组]);

参数说明和 useMemo 一致:

  • 第一个参数:需要缓存的回调函数;
  • 第二个参数:依赖项数组,只有依赖项改变时,才会生成新的函数实例;否则返回缓存的函数实例。

4. 实战优化:解决“子组件无效重渲染”问题

用 useCallback + memo 优化前面的子组件重渲染案例:

import { useState, memo, useCallback } from 'react'; // 导入 memo 和 useCallback

// 用 memo 包装子组件,优化重渲染
const Child = memo(({ count, handleClick }) => {
  console.log('子组件重新渲染');
  return (
    <div onClick={子组件:{count}
  );
});

export default function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  
  // 用 useCallback 缓存回调函数,依赖项只有 count
  const handleClick = useCallback(() => {
    console.log('点击子组件');
  }, [count]); // 只有 count 改变时,才生成新的函数实例

  return (
    <button onClick={ setCount(count + 1)}>count+1(关联子组件)<button onClick={ => setNum(num + 1)}>num+1(无关子组件)<Child count={count} handleClick={handleClick} />
    
  );
}

优化后效果:

  • 点击「count+1」:count 改变,handleClick 的依赖项变化,生成新的函数实例,子组件 props 改变,触发重渲染;
  • 点击「num+1」:num 改变,父组件重新执行,但 handleClick 依赖项(count)未变,返回缓存的函数实例,子组件 props 未变,不触发重渲染。

5. 注意点

  • useCallback 必须和 memo 配合使用:如果子组件没有用 memo 包装,即使缓存了回调函数,子组件依然会跟随父组件重渲染;
  • 依赖项数组要准确:如果回调函数中用到了父组件的状态/属性,必须写进依赖项数组,否则会出现“闭包陷阱”(拿到旧的状态值);
  • useCallback 缓存的是“函数实例”,不是函数的执行结果(和 useMemo 本质区别)。

四、useMemo 与 useCallback 核心区别(必记)

很多人会混淆这两个 Hook,用一张表快速区分:

Hook核心功能缓存内容使用场景
useMemo优化计算逻辑,避免无效计算计算结果(值)昂贵计算、列表过滤、数据处理
useCallback优化子组件重渲染,避免无效渲染回调函数(函数实例)父组件向子组件传递回调函数

一句话总结:useMemo 缓存“值”,useCallback 缓存“函数” ,两者都是为了减少不必要的执行,提升 React 组件性能。

五、实战避坑指南

1. 不要盲目优化

React 本身的渲染性能已经很好,对于简单组件、简单计算,无需使用 useMemo 和 useCallback——缓存本身也需要消耗内存,过度优化反而会增加性能负担。

建议:只有当你明确遇到“计算卡顿”“子组件频繁重渲染”时,再进行优化。

2. 依赖项数组不能乱填

  • 不要空数组:空数组表示“永远不更新”,如果计算/函数依赖某个状态,会导致数据不一致;
  • 不要漏填依赖:如果计算/函数中用到了某个状态/属性,必须写进依赖项数组;
  • 不要多填依赖:无关的依赖会导致不必要的重新计算/函数更新。

3. 配合其他优化手段

useMemo 和 useCallback 不是唯一的性能优化方式,还可以配合:

  • memo:优化子组件重渲染;
  • useEffect 清理函数:避免内存泄漏;
  • 拆分组件:将复杂组件拆分为多个小组件,减少重渲染范围。

六、总结

useMemo 和 useCallback 是 React 性能优化的“黄金搭档”,核心都是通过“缓存”减少不必要的执行:

  1. 当有昂贵计算,且计算依赖特定状态时,用 useMemo 缓存计算结果;
  2. 当需要向子组件传递回调函数,且希望避免子组件无效重渲染时,用 useCallback 缓存函数实例,配合 memo 使用。

记住:性能优化的核心是“解决实际问题”,而不是盲目使用 API。先定位性能瓶颈,再选择合适的优化方式,才能写出高效、流畅的 React 组件。

最后,把文中的代码复制到本地,亲自运行一遍,感受优化前后的差异,你会对这两个 Hook 有更深刻的理解