React性能优化:memo、useMemo和useCallback全解析

开篇:你是否遇到过这种"卡顿"?

想象一个场景:

你在开发一个电商网站 ,有一个商品列表组件 ,还有一个顶部的计数器按钮

你点一下计数器 → 计数器变了

结果呢?整个商品列表也重新渲染了一遍

如果商品只有 10 个,你感觉不到。

但如果商品有 1000 个,而且每个都要做复杂的过滤、排序、渲染,你就会明显感觉到:点一下按钮,卡一下

这就是 React 默认的"无脑重渲染"机制 导致的性能浪费。

怎么办?

用 memo、useMemo 和 useCallback!

它们就是 React 给我们准备的**"缓存神器"**,今天一次性讲清楚,看完再也不迷糊。

一、三个核心 API 一句话记住(新手必背)

先上干货,不用死记硬背,理解就好:

  • memo :缓存组件,props 不变就不重新渲染

  • useMemo :缓存计算结果,依赖不变就不重新计算

  • useCallback :缓存函数,依赖不变就不生成新函数

划重点:它们都是性能优化工具,不是必写代码!只有组件卡顿、出现不必要的重复渲染/计算时,再用才有用。普通简单组件用了反而会增加性能开销,得不偿失。

二、memo:给组件"贴保鲜膜",防止无故重渲染

2.1 它是干嘛的?

memo(全称 memory)是一个高阶组件,简单说就是给函数组件套一层"保护罩",让它变得"聪明":只有接收的 props 真正发生变化时,才重新渲染;props 没变,就直接复用上次的渲染结果。

2.2 没有 memo 会怎样?

React 有个默认机制:父组件重新渲染,所有子组件都会无条件跟着重新渲染,哪怕子组件的 props 一点没变。

看这个简单示例:

javascript 复制代码
// 普通子组件(无 memo)
function NormalChild() {
  console.log('🔴 普通子组件渲染了!');
  return <div>普通子组件</div>;
}

// 父组件
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <NormalChild />
    </div>
  );
}

你点一下"+1"按钮,父组件更新,哪怕 NormalChild 没有接收任何 props,也会跟着渲染,控制台会一直打印"🔴 普通子组件渲染了!"------ 这就是纯粹的性能浪费。

2.3 加上 memo 后,效果立竿见影

javascript 复制代码
import { memo } from 'react';

// 被 memo 包裹的子组件
const MemoChild = memo(() => {
  console.log('🟢 Memo子组件渲染了!');
  return <div>Memo子组件</div>;
});

// 父组件不变
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <MemoChild />
    </div>
  );
}

现在再点"+1",父组件更新,但 MemoChild 完全无动于衷,控制台不会再打印新的日志 ------ 因为它的 props 没变,memo 帮它"跳过"了重渲染。

2.4 什么时候用 memo?

  • 子组件是纯展示型(只负责渲染,不做复杂逻辑),且 props 很少变化;

  • 子组件渲染开销很大(比如长列表、复杂图表、嵌套层级多的表单);

  • 父组件经常重新渲染,但子组件的 props 基本不变。

2.5 一个致命坑点!(新手必踩)

memo 对 props 的比较是浅比较 (Shallow Comparison)。如果你的 props 是对象、数组、函数这类引用类型,哪怕内容完全一样,但引用地址变了,memo 也会认为"props 变了",从而触发子组件重新渲染。

尤其是函数类型的 props,这是最容易踩坑的场景 ------ 哪怕你写的函数逻辑完全没变,父组件每次渲染都会生成一个新的函数,导致 memo 直接失效。

比如这段错误示范:

javascript 复制代码
// 子组件(memo 包裹)
const MemoChild = memo(({ handleClick }) => {
  console.log('🟢 Memo子组件渲染了!');
  return <button onClick={handleClick}>点击</button>;
});

// 父组件
function Parent() {
  const [count, setCount] = useState(0);

  // 父组件每次渲染,都会创建一个新的 handleClick 函数!
  const handleClick = () => {
    console.log('点击了');
  };

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <MemoChild handleClick={handleClick} />
    </div>
  );
}

你点"+1",父组件更新 → 生成新的 handleClick 函数 → memo 浅比较发现函数引用变了 → 子组件被迫重新渲染。明明加了 memo,却没起到作用 ------ 这时候,就需要 useCallback 登场了!

三、useCallback:专门缓存函数,解决 memo 失效问题

3.1 它是干嘛的?

useCallback 是一个 React Hook,核心作用只有一个:缓存函数本身。让函数在"依赖项不变"的情况下,永远保持同一个引用地址,不会因为父组件渲染而重新生成新函数。

它就像给函数"存了个档",只要依赖没变,每次取出来的都是同一个"版本",这样 memo 做浅比较时,就会认为 props 没变,从而阻止子组件重渲染。

3.2 基本用法

javascript 复制代码
import { useCallback } from 'react';

// 语法:useCallback(函数体, 依赖数组)
const 缓存后的函数 = useCallback(() => {
  // 函数逻辑
}, [依赖项1, 依赖项2]);

和 useMemo 类似,依赖数组是关键:

  • 依赖数组为空 []:函数只会生成一次,永远不变;

  • 依赖数组有值:只有依赖项发生变化时,才会生成新函数。

3.3 搭配 memo 使用,彻底解决函数 props 问题

把上面的错误示范改成正确写法,只需要加一行 useCallback:

javascript 复制代码
import { memo, useCallback, useState } from 'react';

// 子组件(memo 包裹,不变)
const MemoChild = memo(({ handleClick }) => {
  console.log('🟢 Memo子组件渲染了!');
  return <button onClick={handleClick}>点击</button>;
});

// 父组件(用 useCallback 缓存函数)
function Parent() {
  const [count, setCount] = useState(0);

  // 用 useCallback 缓存 handleClick,依赖为空,永远是同一个函数
  const handleClick = useCallback(() => {
    console.log('点击了');
  }, []); // 依赖为空,函数引用不变

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <MemoChild handleClick={handleClick} />
    </div>
  );
}

现在再点"+1",父组件更新,但 handleClick 的引用没变 → memo 浅比较通过 → 子组件不再重新渲染!完美解决问题。

3.4 什么时候用 useCallback?

  • 子组件用 memo 包裹,且需要接收函数类型的 props(最核心场景);

  • 函数需要作为依赖项传入其他 Hook(比如 useEffect),避免每次渲染都触发 useEffect;

  • 父组件频繁更新,且函数逻辑不变,需要避免函数重复生成。

3.5 常见误区

❌ 不要给所有函数都加 useCallback:简单函数(比如 console.log)本身生成开销很小,加 useCallback 反而会增加缓存的开销,得不偿失;

❌ 依赖数组不要漏写:如果函数里用到了父组件的 state、props,一定要把它们加入依赖数组,否则会出现"闭包陷阱"(函数里拿到的是旧的 state/props);

✅ 正确示例(函数用到 state,依赖数组加 count):

javascript 复制代码
const handleClick = useCallback(() => {
  console.log('count:', count); // 用到了 count
}, [count]); // 必须把 count 加入依赖

四、useMemo:给计算结果"存草稿",避免重复计算

4.1 它是干嘛的?

useMemo 也是一个 React Hook,核心作用是缓存复杂计算的结果。如果组件里有耗时的计算(比如大数据过滤、排序、多轮循环),每次渲染都重新计算会非常浪费性能,用 useMemo 可以让计算结果"存档",依赖不变就不用重新算。

形象比喻:就像你做数学题,算完一次把结果写在草稿纸上,下次再用到时,直接看草稿纸,不用再重新算一遍。

4.2 没有 useMemo 会怎样?

看这段代码:

javascript 复制代码
function Parent() {
  const [list, setList] = useState(/* 1000条数据 */);

  // 复杂计算:过滤并排序1000条数据
  const filteredList = list.filter(item => item.status === 1).sort((a, b) => a.time - b.time);

  return <MemoChild list={filteredList} />;
}

父组件每次渲染(哪怕只是修改了一个无关的 state),filteredList 都会重新计算一次 ------ 1000条数据的过滤+排序,次数多了就会卡顿。

4.3 加上 useMemo 后

javascript 复制代码
import { useMemo, useState } from 'react';

function Parent() {
  const [list, setList] = useState(/* 1000条数据 */);

  // 用 useMemo 缓存计算结果,只有 list 变了才重新算
  const filteredList = useMemo(() => {
    return list.filter(item => item.status === 1).sort((a, b) => a.time - b.time);
  }, [list]); // 依赖 list

  return <MemoChild list={filteredList} />;
}

现在,只有 list 发生变化时,才会重新执行过滤和排序;list 不变,直接复用上次的计算结果 ------ 性能瞬间提升。

4.4 什么时候用 useMemo?

  • 组件内部有明显耗时的计算(过滤、排序、大数据处理、复杂格式化);

  • 计算结果要传给 memo 子组件做 props(搭配 memo 使用,双重优化);

  • 计算逻辑重复执行,且结果在依赖不变时不会变化。

4.5 与 useCallback 的区别

很多新手会混淆 useMemo 和 useCallback,一句话分清:

  • useMemo:缓存函数的返回值(计算结果);

  • useCallback:缓存函数本身(函数引用)。

简单说:需要缓存"值",用 useMemo;需要缓存"函数",用 useCallback。

五、黄金搭档:memo + useCallback + useMemo(工作中最常用)

实际开发中,这三个 API 很少单独使用,搭配起来才能实现最大化的性能优化,尤其是处理复杂列表、大数据场景时。

核心组合逻辑:

  1. 子组件用 memo 包裹,防止不必要的重渲染;

  2. 给子组件传的函数 props,用 useCallback 缓存,避免 memo 失效;

  3. 给子组件传的计算型 props,用 useMemo 缓存,避免重复计算。

完整可运行 Demo(包含所有优化)

复制这段代码,在 Create React App 中直接运行,打开控制台点击按钮,就能直观看到优化效果:

javascript 复制代码
import { useState, memo, useMemo, useCallback } from 'react';

// 1. memo 缓存子组件(接收函数和计算结果 props)
const ListChild = memo(({ items, handleItemClick }) => {
  console.log('🟢 列表子组件渲染');
  return (
    <ul style={{ marginTop: 20 }}>
      {items.map(item => (
        <li key={item.id} onClick={() => handleItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

// 2. 父组件
export default function App() {
  const [list, setList] = useState([
    { id: 1, name: "苹果", status: 1 },
    { id: 2, name: "香蕉", status: 0 },
    { id: 3, name: "橙子", status: 1 },
    { id: 4, name: "葡萄", status: 1 }
  ]);
  const [count, setCount] = useState(0);

  // 3. useMemo 缓存复杂计算结果(过滤状态为1的商品)
  const filteredItems = useMemo(() => {
    console.log('🧮 useMemo 计算执行');
    return list.filter(item => item.status === 1).sort((a, b) => a.id - b.id);
  }, [list]); // 依赖 list,只有 list 变才重新计算

  // 4. useCallback 缓存函数(传给子组件的点击事件)
  const handleItemClick = useCallback((id) => {
    console.log('点击了商品:', id);
  }, []); // 依赖为空,函数引用不变

  console.log('------------------ 父组件渲染 ------------------');

  return (
    <div style={{ padding: 20 }}>
      <h1>count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>+1(无关操作)</button>

      <h3>过滤后的商品列表(只显示状态为1的)</h3>
      <ListChild items={filteredItems} handleItemClick={handleItemClick} />
    </div>
  );
}

Demo 效果说明(打开控制台查看)

  • 点击"+1"按钮,父组件渲染(控制台打印"父组件渲染");

  • ListChild 子组件不会渲染(控制台不打印"列表子组件渲染")------ 因为 memo + useCallback + useMemo 一起生效了;

  • useMemo 里的计算不会执行(控制台不打印"useMemo 计算执行")------ 因为 list 没变;

  • 只有当 list 变化时(比如添加、删除商品),filteredItems 才会重新计算,ListChild 才会重新渲染。

六、核心补充:useCallback 依赖相关(新手必看)

针对 useCallback 的依赖数组,直接讲核心(可直接用于开发参考):

1. 依赖为空 [] 的含义

当 useCallback 的依赖数组写 [] 时,意味着这个函数不依赖父组件任何 state、props 等变量,函数逻辑固定不变。

示例:

javascript 复制代码
const handleItemClick = useCallback((id) => { console.log('点击了商品:', id); }, []);

这个函数只打印商品id,不用到父组件任何变量,因此仅在父组件第一次渲染时生成一次,后续父组件无论怎么重新渲染,函数引用都不会变,能配合 memo 阻止子组件无故重渲染。

2. 函数依赖变量时的写法(核心原则:用到什么,加什么)

如果函数逻辑中用到了父组件的 state、props,必须把这些变量全部加入依赖数组,否则会出现"闭包陷阱"(函数中拿到的是旧的变量值)。

场景1:依赖父组件 state(如 count)
javascript 复制代码
const [count, setCount] = useState(0);
// 用到 count,必须加入依赖
const handleClick = useCallback(() => {
  console.log('当前count:', count);
  setCount(count + 1);
}, [count]);
场景2:依赖父组件 props(如 title)
javascript 复制代码
function Parent({ title }) {
  // 用到 props.title,加入依赖
  const handleShowTitle = useCallback(() => {
    alert(`标题:${title}`);
  }, [title]);
  return &lt;MemoChild onShowTitle={handleShowTitle} /&gt;;
}
场景3:依赖多个变量(state + props)
javascript 复制代码
function Parent({ title }) {
  const [count, setCount] = useState(0);
  // 用到 title 和 count,全部加入依赖
  const handleCombine = useCallback(() => {
    console.log(`标题:${title},count:${count}`);
  }, [title, count]);
  return <MemoChild onCombine={handleCombine} />;
}

关键总结

useMemo 依赖数组规则,和 useCallback 完全通用:

  • 无依赖变量 → 写 [],计算仅执行一次,永久复用;
  • 有依赖变量 → 用到的变量全部加入,变量变则重新计算。

七、避坑指南(新手必看,少走弯路)

1. 不要滥用优化 API

memo、useMemo、useCallback 都有轻微的性能开销(创建缓存、比较依赖)。对于简单组件、简单函数、简单计算,完全不需要用 ------ 比如一个只显示文字的子组件,加 memo 反而会变慢。

2. 先找瓶颈,再优化

不要上来就给所有组件、函数、计算都加缓存。先用 React 官方的 React DevTools 的 Profiler 功能,分析出到底是哪个组件渲染慢、哪个计算重复执行,再针对性优化。

3. 依赖数组一定要写完整

useMemo 和 useCallback 的依赖数组,必须包含函数体里用到的所有 state、props ------ 漏写会导致闭包陷阱,比如拿到旧的 state 值,出现 bug。

4. 引用类型 props 必须缓存

如果给 memo 子组件传的 props 是对象、数组、函数,一定要用 useMemo(对象/数组)、useCallback(函数)缓存,否则 memo 会失效。

5. memo 不是"万能的"

memo 只能优化"props 不变时的重渲染",如果子组件本身依赖了全局状态(比如 Redux)、context,或者自己有 state 变化,memo 是无法阻止它重渲染的 ------ 它只管 props。

八、最终总结(一张表看懂所有区别)

API 核心作用 缓存对象 解决的问题 常用搭配
memo 缓存组件 组件实例 子组件不必要的重渲染 useCallback、useMemo
useMemo 缓存计算结果 函数返回值 复杂计算重复执行 memo(给子组件传计算值)
useCallback 缓存函数 函数本身 函数 props 导致 memo 失效 memo(给子组件传函数)

最后记住一句话:性能优化是为了解决实际问题,而不是为了优化而优化。先写出能正常运行的代码,再根据实际卡顿情况,用这三个神器针对性优化,你的 React 项目就能变得更流畅、更高效。

相关推荐
兔子零10242 小时前
Claude Code 都把宠物养进终端了,我做了一个真正能长期玩的中文宠物游戏
前端·游戏·游戏开发
xiaotao1312 小时前
Vite 与 Webpack 开发/打包时环境变量对比
前端·vue.js·webpack
摆烂工程师2 小时前
教你如何查询 Codex 最新额度是多少,以及 ChatGPT Pro、Plus、Business 最新额度变化
前端·后端·ai编程
捧月华如2 小时前
响应式设计原理与实践:适配多端设备的前端秘籍
前端·前端框架·json
笨笨狗吞噬者2 小时前
VSCode 插件推荐 Copy Filename Pro,快速复制文件、目录和路径的首选
前端·visual studio code
Armouy2 小时前
Electron:核心概念、性能优化与兼容问题
前端·javascript·electron
F2E_Zhangmo2 小时前
react native如何发送蓝牙命令
javascript·react native·react.js
淡笑沐白2 小时前
ECharts入门指南:数据可视化实战
前端·javascript·echarts
光影少年2 小时前
RN中如何处理权限申请(相机、相册、定位、存储)?使用第三方库还是原生封装?
react native·react.js·掘金·金石计划