React性能优化三剑客:memo、useMemo和useCallback详解

2025-07-30
22188 分钟
...

前言

在React开发中,性能优化是一个永恒的话题。今天我们就来深入探讨React提供的三个重要性能优化工具:memouseMemouseCallback,它们如何帮助我们构建更高效的React应用。

1. 为什么需要性能优化? 🤔

React的核心机制是当组件的state或props发生变化时,组件会重新渲染。但有时这种重新渲染是不必要的:

  • 父组件更新导致所有子组件重新渲染,即使子组件的props没有变化
  • 复杂计算在每次渲染时重复执行,消耗大量资源
  • 函数引用在每次渲染时重新创建,导致依赖该函数的子组件不必要更新

这就是我们的三剑客登场的时候了!✨

2. React.memo:组件记忆大师 🧠

基本用法

React.memo是一个高阶组件,用于包装函数组件,使其仅在props发生变化时才重新渲染。

import { memo } from 'react';

const MyComponent = memo(function MyComponent(props) {
  
});

工作原理

  • 🔄 默认情况下,当父组件重新渲染时,所有子组件也会重新渲染
  • memo会记住组件上一次渲染的结果,并在下一次渲染时比较新旧props
  • 🔍 如果props没有变化,则跳过重新渲染,直接复用上一次的结果

示例分析

在提供的代码中,Button组件使用了memo

const Button = ({num}) => {
    
}
export default memo(Button)

这样,当App组件的count状态变化时(与Button无关),Button不会重新渲染。但是当App组件的num状态发生改变时Button会重新渲染

面试考点

Q: React.memo是如何工作的?它与PureComponent有什么不同?

A:

  • React.memo是用于函数组件的高阶组件,通过浅比较props来决定是否重新渲染
  • PureComponent是用于类组件的类似功能,通过浅比较props和state
  • 两者都使用浅比较,但memo只比较props,且用于函数组件

3. useMemo:复杂计算的缓存专家 💾

基本用法

useMemo用于缓存计算结果,避免每次渲染时都重新计算。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

工作原理

  • 🧮 只有在依赖项数组[a, b]中的值发生变化时,才会重新计算
  • 💡 否则直接返回上一次缓存的结果
  • ⚡ 适用于计算开销大的操作

示例分析

App组件中,expensiveComputation是一个计算密集型函数:

const expensiveComputation = (n) => {
    for(let i = 0 ;  i < 1000000000; i++) { i++; }
    return n * 2
}
const result = useMemo(() => expensiveComputation(num), [num])

使用useMemo后,只有当num变化时才会重新计算,避免了不必要的计算开销。

面试考点

Q: useMemo和useCallback有什么区别?

A:

  • useMemo缓存的是计算结果(值)
  • useCallback缓存的是函数本身(引用)
  • 两者都依赖依赖项数组来决定是否重新计算/创建

4. useCallback:函数引用的守护者 🔄

基本用法

useCallback用于缓存函数引用,避免每次渲染时都创建新的函数。

const memoizedCallback = useCallback(() => {
    doSomething(a, b);
}, [a, b]);

工作原理

  • 🔗 在依赖项不变的情况下,返回相同的函数引用
  • 🆕 依赖项变化时,返回新创建的函数
  • 🧩 特别适用于将函数作为props传递给优化过的子组件

示例分析

App组件中:

const handleClick = useCallback(() => {
    console.log('handleClick')
}, [num])

<Button num={num} onClick={handleClick}>Click Me </Button>

这样,只有当num变化时,handleClick函数才会重新创建,避免了Button组件因函数引用变化而不必要的重新渲染。

面试考点

Q: 为什么即使函数内容没变,每次渲染时函数引用也会变化?

A:

  • 在JavaScript中,函数是对象,每次定义都会创建一个新的引用
  • 即使函数内容完全相同,() => {} === () => {}也会返回false
  • 这就是为什么我们需要useCallback来保持稳定的函数引用

5. 三者的关系与协作 🤝

工具缓存目标主要用途依赖项作用
React.memo组件渲染结果避免子组件不必要的重新渲染比较props决定是否重新渲染
useMemo计算值避免重复复杂计算决定何时重新计算
useCallback函数引用保持稳定的函数引用决定何时重新创建函数

它们通常一起使用来优化性能:

  1. useMemo缓存计算值
  2. useCallback缓存事件处理函数
  3. memo包装子组件避免不必要的渲染

6. 使用注意事项 ⚠️

  1. 不要过度优化:这些API本身也有开销,只在确实需要时使用
  2. 依赖项要完整:确保依赖项数组包含所有变化会影响结果的变量
  3. 浅比较的局限memo的浅比较可能无法捕获深层对象的变化
  4. 不是银弹:这些工具无法解决所有性能问题,组件设计本身更重要

7. 性能优化最佳实践 🏆

  1. 合理拆分组件:将频繁变动的部分与稳定部分分离
  2. 状态提升与下降:将状态放在真正需要它的组件中
  3. 使用React DevTools:分析组件渲染情况,找到真正需要优化的地方
  4. 考虑虚拟化长列表:对于大数据列表使用react-window等库

8. 常见面试题与答案 📝

Q1: 什么时候应该使用useMemo和useCallback?

A:

  • 当计算成本很高时使用useMemo
  • 当需要保持稳定的函数引用(如传递给优化子组件的回调)时使用useCallback
  • 当组件的重新渲染代价很高且props经常不变时使用memo

Q2: useMemo能完全替代useCallback吗?

A: 技术上可以,因为useCallback(fn, deps)等同于useMemo(() => fn, deps),但语义上应该分开使用,代码可读性更好。

Q3: 为什么我的memo组件还是重新渲染了?

A: 可能原因:

  • props中的对象/数组每次都是新的引用
  • 传递了内联函数(未用useCallback)
  • 使用了children prop(children总是被视为新的)
  • props中有未在依赖项中列出的变化值

Q4: useMemo和useEffect有什么区别?

A:

  • useMemo在渲染期间同步执行,用于计算值
  • useEffect在渲染后异步执行,用于副作用操作
  • useMemo的结果可以直接用于渲染,而useEffect不能返回值

9. 总结 🎯

memouseMemouseCallback是React性能优化的强大工具,但它们不是万能的。理解它们的工作原理和适用场景,结合合理的组件设计,才能真正提升应用性能。记住:先确保功能正确,再考虑性能优化,不要过早优化!

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
react2025-08-03
在 React 中实现倒计时功能会有什么坑
倒计时 倒计时是一个非常常见的业务场景,但是在 React 中实现起来,却不算简单。 首先我们来看这段倒计时代码,它能否正常执行? 来看看实际表现效果 可以看到计时器在不断执行,但是 的值却没有变。 这是一个很经典的问题:React 闭包陷阱 。 我们来详细分析下: 的依赖数组 是空的,这意味着 effect 只会在组件挂载时执行一次,而不会在 状态更新时重新执行。所以 回调函数中捕获的 值始终是初始值 。 这个问题解决起来也很简单,有...
学习面试
react2024-12-06
React 19 终于发布,一大波新功能正式升级
这篇文章主要介绍了 React 19 的新功能,包括 Actions 自动处理数据突变相关状态,新增的 useActionState、useFormStatus、useOptimistic 等 hook,新的 use API 读取渲染资源,新的 React DOM 静态 API 生成静态站点,以及 React 服务器组件中的 RSC 和 RSA 等内容。同时提到后续会分享针对旧版功能的优化和改良。 关联问题: React 19 性能如何...
学习面试
react2024-09-18
React | 浏览器刷新,会执行 useEffect 的 卸载函数吗?
React | 浏览器刷新,会执行 useEffect 的 卸载函数吗? 需求描述 后端在跑一个长期的异步上传任务,在任务执行过程中,用户刷新页面(f5),此时产品希望能够发送一个 kill 任务,杀掉这个任务。 拆解 需求合理性 其他情况是否存在类似:比如关闭 tab,关闭浏览器,突然断网等操作,等浏览器层级或者异常的用户行为 前端是否应该响应这种行为:对于更深层次的需求处理 根本问题点,或者说为什么会存在这个需求:上一个任务会加锁,...
学习面试
react2024-07-29
Effect:渲染之外的副作用
React 借鉴了许多函数式编程的理念,使得它在构建复杂用户界面时能够保持代码的简洁、可维护和可测试性。函数式编程在很大程度上依赖于纯函数 纯函数(Pure Function)是指在计算机科学和函数式编程中使用的一类函数。一个函数如果具备以下两个主要特性,我们就可以称之为纯函数: 1. 引用透明性:纯函数在相同的输入下总是产生相同的输出。也就是说,函数的输出仅依赖于其输入参数,不依赖于任何外部状态或变量。 2. 无副作用:纯函数不会改变...
学习面试
nuxt2025-10-22
Nuxt SSR 与 Next.js SSR
🧭 引言 在现代前端生态中, Nuxt(Vue) 与 Next.js(React) 是最成熟的两大全栈框架。 它们都具备 SSR(Server Side Rendering) 能力,但实现原理与运行机制差异巨大。 如果你在使用 Nuxt 构建 SSR 项目,却发现“路由跳转后不再 SSR”, 而在 Next.js 中同样操作却会重新发起服务端渲染——这不是 bug,而是 理念差异 。 本文将详细对比这两种框架的 SSR 行为,帮助你彻...
学习面试
vue2025-10-22
shallowRef 与 ref 的区别、场景与坑位
结论先行: 会 深度 地把你放进去的对象转成响应式(递归代理),因此"对象内部的改动"也会触发视图更新。 只在 顶层 做依赖追踪: 更换 才触发更新 ;若你只是"在原对象上改属性",不会触发,除非手动 。 1\. 快速对比 维度 追踪粒度 深度(递归) 仅顶层 对象内部属性变更 会触发更新 不会 触发(除非 或重新赋值) 性能 有递归 & 依赖跟踪开销 更轻量,适合大对象/第三方实例 适用对象 普通标量、普通对象、数组 第三方实例(图表...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录