React性能优化三剑客之useMemo、memo与useCallback详解

 更新时间:2026年04月09日 10:38:23   作者:晴栀ay  
useMemo和useCallback是React 中用于性能优化的两个重要 Hook,它们基于相同的依赖项比较机制,但适用场景不同,这篇文章主要介绍了React性能优化三剑客之useMemo、memo与useCallback的相关资料,需要的朋友可以参考下

"在React世界里,每一次不必要的渲染都是对用户体验的无声谋杀。" - 某位不愿透露姓名的性能优化工程师

前言:性能优化的必然性

当你第一次接触React时,可能被它的声明式编程和组件化思想所吸引。但随着应用复杂度增长,你是否曾经历过这样的场景:一个简单的状态更新导致了整个页面"颤动",或者滚动列表时出现明显卡顿?这些现象背后,往往隐藏着不必要的重复渲染和昂贵计算。

今天,我们将一起探索React性能优化的三大利器:useMemomemouseCallback。它们就像是React世界的"防抖开关",帮我们精确控制什么该渲染,什么该缓存。

一、useMemo

1.1 问题场景:看不见的性能杀手

想象一个包含搜索功能的商品列表组件。当用户在搜索框中输入关键词时,我们过滤商品列表。同时,页面上还有其他无关的状态(比如一个计数器)。代码可能如下:

function App() {
  const [count, setCount] = useState(0);
  const [keyword, setKeyword] = useState('');
  const list = ['apple', 'banana', 'orange', 'pear'];
  // 问题就在这里!
  const filterList = list.filter(item => {
    console.log('filter 执行');
    return item.includes(keyword);
  });
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      <input 
        type="text" 
        value={keyword} 
        onChange={e => setKeyword(e.target.value)} 
      />
      {filterList.map(item => (
        <li key={item}>{item}</li>
      ))}
    </div>
  )
}

当你点击"count + 1"按钮时,控制台会显示"filter 执行",尽管搜索关键词根本没有变化!这意味着每次任何状态更新,这个过滤操作都会重新执行。

对于简单列表这可能影响不大,但如果过滤操作需要遍历数千条数据,或者执行复杂计算,性能问题就会凸显。

1.2 useMemo

useMemo就像一个智能缓存器,只在依赖项变化时重新计算:

const filterList = useMemo(() => {
  console.log('filter 执行');
  return list.filter(item => item.includes(keyword));
}, [keyword]); // 仅当keyword变化时重新计算

现在,当你更新count时,过滤操作不会重新执行,只有keyword变化时才会重新计算。这显著减少了不必要的计算开销。

1.3 优化昂贵计算:真实世界的例子

考虑一个需要计算大量数据的场景:

function slowSum(n) {
  console.log('计算中...');
  let sum = 0;
  for(let i = 0; i <= n*10000000; i++) {
    sum += i;
  }
  return sum;
}
function App() {
  const [num, setNum] = useState(0);
  // 优化前:每次组件渲染都会执行slowSum
  const result = slowSum(num);
  // 优化后:只在num变化时重新计算
  const result = useMemo(() => {
    return slowSum(num);
  }, [num]);
  return (
    <div>
      <button onClick={() => setNum(num + 1)}>num + 1</button>
      <p>num: {result}</p>
    </div>
  )
}

点击按钮时,你只会看到一次"计算中..."的日志,而不是每次渲染都计算。对于真正的计算密集型任务,这种优化可能是应用流畅与否的关键。

二、React.memo

2.1 组件重渲染的连锁反应

React中,当父组件状态更新时,所有子组件默认都会重新渲染,无论它们的props是否变化。这就像一栋公寓楼中,一户人家换了灯泡,整栋楼的住户都被通知要出来看一眼。

function Child({ count }) {
  console.log('child 重新渲染');
  return <div>子组件 count: {count}</div>
}
function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      {num}
      <button onClick={() => setNum(num + 1)}>num + 1</button>
      <Child count={count} />
    </div>
  )
}

当你点击"num + 1"按钮时,Child组件也会重新渲染,尽管它的props(count)没有变化!

2.2 memo

React.memo是一个高阶组件,它对函数组件进行包装,使其仅在props变化时重新渲染:

const Child = memo(({ count }) => {
  console.log('child 重新渲染');
  return <div>子组件 count: {count}</div>
})

现在,点击"num + 1"按钮时,Child组件不会重新渲染,因为它的props没有变化。这就像给子组件装上了智能门锁,只有"真正需要进门的人"才能触发重新渲染。

三、useCallback:函数传递的优化艺术

3.1 隐藏的陷阱:函数引用变化

当父组件向子组件传递回调函数时,会遇到一个隐蔽问题:

const Child = memo(({ count, handleClick }) => {
  console.log('child 重新渲染');
  return <div onClick={handleClick}>子组件 count: {count}</div>
})
function App() {
  const [count, setCount] = useState(0);
  // 每次组件渲染都会创建新函数
  const handleClick = () => {
    console.log('click');
  }
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>count + 1</button>
      <Child count={count} handleClick={handleClick} />
    </div>
  )
}

即使使用了memo,Child组件仍然会在count变化时重新渲染!为什么?因为每次父组件渲染时,handleClick都会创建一个新函数,导致子组件的props发生变化。

3.2 useCallback:函数的稳定引用

useCallback解决了这个问题,它返回一个记忆化的回调函数:

const handleClick = useCallback(() => {
  console.log('click');
}, []); // 依赖数组为空,函数永远不会重新创建

如果回调需要依赖组件内的状态或prop,可以将它们添加到依赖数组中:

const handleClick = useCallback(() => {
  console.log('click', count);
}, [count]); // 仅当count变化时重新创建函数

现在,当count以外的状态变化时,handleClick函数引用保持不变,Child组件不会不必要地重新渲染。

四、三大利器的协同作战

在复杂应用中,这三个优化钩子常常需要协同工作:

const ParentComponent = () => {
  const [searchTerm, setSearchTerm] = useState('');
  const [userList, setUserList] = useState([]);
  const [theme, setTheme] = useState('light');
  // 优化1: 使用useMemo缓存过滤结果
  const filteredUsers = useMemo(() => {
    return userList.filter(user => 
      user.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
  }, [userList, searchTerm]);
  // 优化2: 使用useCallback确保函数引用稳定性
  const handleUserClick = useCallback((userId) => {
    // 处理用户点击
  }, []);
  // 优化3: 使用memo防止不必要的子组件渲染
  const UserList = memo(({ users, onUserClick }) => {
    return (
      <div>
        {users.map(user => (
          <UserItem 
            key={user.id} 
            user={user} 
            onClick={() => onUserClick(user.id)} 
          />
        ))}
      </div>
    );
  });
  return (
    <div>
      <SearchBar value={searchTerm} onChange={setSearchTerm} />
      <ThemeToggle theme={theme} onToggle={setTheme} />
      <UserList users={filteredUsers} onUserClick={handleUserClick} />
    </div>
  );
};

在这个例子中:

  • 当theme变化时,不会重新计算filteredUsers
  • UserList组件只在filteredUsers变化时重新渲染
  • handleUserClick保持稳定的引用,不会导致UserItem不必要的重渲染

五、最佳实践与注意事项

5.1 何时使用,何时放弃?

  • 不要过早优化:先编写清晰的代码,再通过性能分析工具(如React DevTools的Profiler)识别真正的瓶颈
  • 小型计算不必缓存:如果计算非常轻量,useMemo可能带来额外开销
  • 避免依赖数组过大:过度依赖会导致缓存失效频繁,失去优化意义

5.2 常见误区

  1. "所有函数都应该用useCallback包装"  - 错误!只有传递给优化过的子组件(使用memo)的函数才需要
  2. "useMemo可以替代useEffect"  - 错误!useMemo用于计算和返回值,useEffect用于副作用
  3. "依赖数组为空总是最好的"  - 危险!可能导致闭包中使用过期的值

5.3 高级技巧

  • 自定义比较函数:React.memo可以接受第二个参数,自定义props比较逻辑
  • useRef替代方案:对于某些场景,useRef可以作为useCallback的替代方案
  • 结构化克隆:处理复杂对象时,确保依赖项真正反映数据变化

六、性能优化的哲学思考

在追求性能的道路上,我们常常陷入一个误区:过度优化。就像一位厨师不断调整食谱的细微之处,却忘了最重要的是一道菜的整体味道。

React的性能优化应当遵循这样的原则:

  • 可读性优先:代码首先是给人读的,其次才是给机器执行的
  • 问题驱动:只在真正遇到性能问题时才进行优化
  • 平衡之道:在性能和开发体验之间找到平衡点

记住,最优雅的优化是"无需优化"。通过良好架构和合理状态管理,很多性能问题在设计阶段就可避免。

结语

useMemo、memo和useCallback是React性能优化工具箱中的三件利器。它们不是解决所有问题的银弹,而是在特定场景下精准发力的手术刀。

掌握它们的关键在于理解React的渲染机制,识别真正的性能瓶颈,并在恰当的时机应用这些技术。如React核心团队成员Dan Abramov所言:"优化应该像调味品——适量使用可以提升体验,过量则会毁掉整道菜。"

下次当你面对卡顿的UI或缓慢的交互时,不妨回想这三大利器。它们可能不会让你的应用瞬间飞起来,但一定会让用户体验更加丝滑,就像一位隐形的管家,默默确保一切井然有序。

到此这篇关于React性能优化三剑客之useMemo、memo与useCallback的文章就介绍到这了,更多相关React useMemo、memo与useCallback内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解析React ref 命令代替父子组件的数据传递问题

    解析React ref 命令代替父子组件的数据传递问题

    这篇文章主要介绍了React - ref 命令为什么代替父子组件的数据传递,使用 ref 之后,我们不需要再进行频繁的父子传递了,子组件也可以有自己的私有状态并且不会影响信息的正常需求,这是为什么呢?因为我们使用了 ref 命令的话,ref是可以进行状态的传输
    2022-08-08
  • React路由拦截模式及withRouter示例详解

    React路由拦截模式及withRouter示例详解

    这篇文章主要为大家介绍了React路由拦截模式及withRouter示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-08-08
  • 比ant更丰富Modal组件功能实现示例详解

    比ant更丰富Modal组件功能实现示例详解

    这篇文章主要为大家介绍了比ant更丰富Modal组件功能实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • react国际化react-intl的使用

    react国际化react-intl的使用

    这篇文章主要介绍了react国际化react-intl的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-05-05
  • react的hooks防抖和节流的具体实现

    react的hooks防抖和节流的具体实现

    本文主要介绍了react的hooks防抖和节流的具体实现,解释了为何需要特别处理闭包问题,并提供了具体实现方案,文章还强调了使用useRef保存最新函数的重要性,以及两种防抖节流方法的区别与应用场景,感兴趣的可以了解一下
    2026-05-05
  • React中的refs的使用教程

    React中的refs的使用教程

    本篇文章主要介绍了React中的refs的使用教程,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • React ts模式使用http-proxy-middleware代理时访问报404问题

    React ts模式使用http-proxy-middleware代理时访问报404问题

    这篇文章主要介绍了React ts模式使用http-proxy-middleware代理时访问报404问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-07-07
  • 深入理解React与闭包的关系

    深入理解React与闭包的关系

    本文将深入探讨React与闭包之间的关系,我们将首先介绍React和闭包的基本概念,然后详细解释React组件中如何使用闭包来处理状态和作用域的问题,希望通过本文的阅读,你将对React中闭包的概念有更深入的理解,并能够在开发React应用时更好地应用闭包
    2023-07-07
  • React组件内事件传参实现tab切换的示例代码

    React组件内事件传参实现tab切换的示例代码

    本篇文章主要介绍了React组件内事件传参实现tab切换的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-07-07
  • react vite使用import.meta.glob批量导入路由方式

    react vite使用import.meta.glob批量导入路由方式

    文章介绍了如何通过动态引入模块中的路由信息,简化了传统的路由管理方式,无需单独引入每个模块的路由
    2025-10-10

最新评论