React 性能优化:别让组件瞎渲染!这 3 个 Hook 能救命

写 React 组件时,你有没有遇到过这种情况:明明只改了一个数字,却发现控制台里一堆组件在 "瞎渲染"------ 打印日志刷个不停,页面偶尔还卡顿?其实这不是组件 "不听话",而是你没告诉它 "什么时候该偷懒"。

父子组件的渲染顺序

组件的渲染过程分为 "执行阶段" 和 "挂载阶段",顺序完全相反,这是理解渲染优化的基础。

1. 执行阶段:从外到内(父组件先 "动手")

当父组件状态变化时,React 会先执行父组件的函数,再执行子组件的函数。就像盖房子时 "先搭框架,再砌墙":

jsx 复制代码
function Parent() {
  console.log('Parent 执行');
  return <Child />;
}

function Child() {
  console.log('Child 执行'); 
  return <div>子组件</div>;
}

打印顺序Parent 执行Child 执行

原因:父组件的函数不执行,就不知道要渲染哪些子组件。只有父组件先 "算清楚要渲染什么",子组件才有机会被执行。

2. 挂载阶段:从内到外(子组件先 "完工")

当组件真正渲染到 DOM 时,顺序是先完成子组件的挂载,再完成父组件的挂载。就像盖房子时 "先装窗户,再装大门":

jsx 复制代码
function Parent() {
  useEffect(() => {
    console.log('Parent 挂载完成'); 
  }, []);

  return <Child />;
}

function Child() {
  useEffect(() => {
    console.log('Child 挂载完成');
  }, []);

  return <div>子组件</div>;
}

打印顺序Child 挂载完成Parent 挂载完成

原因:DOM 树是嵌套结构,必须先渲染内层子节点,才能完成外层父节点的组装。

关键结论:

  • 状态变化时,父组件先执行,子组件后执行(执行阶段从外到内);
  • 渲染到 DOM 时,子组件先挂载,父组件后挂载(挂载阶段从内到外);
  • 如果父组件执行时发现子组件的 props 没变,就可以阻止子组件 "白干活"------ 这就是React.memo的优化思路。

问题来了:子组件为什么会 "瞎渲染"?

知道了渲染顺序,再看一个常见问题:父组件的某个状态变化时,明明和子组件无关,子组件却跟着重新执行(瞎渲染)。

先看一个简单的例子:

jsx 复制代码
// 父组件
function App() {
  const [count, setCount] = useState(0); // 父组件的count状态
  const [num, setNum] = useState(0);     // 父组件的num状态

  console.log('App 执行'); // 父组件执行时打印

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>count+1</button>
      <button onClick={() => setNum(num + 1)}>num+1</button>
      {/* 子组件只用到了num和handleClick,和count无关 */}
      <Button num={num} />
    </div>
  );
}


// Button组件(子组件)
const Button = ({ num}) => {
  console.log('Button render'); // 子组件渲染时打印
  return <button >num: {num}</button>;
};

当点击 "count+1" 按钮时,count变化会导致App组件重新渲染。这时候你会发现,Button组件的console.log('Button render')也会执行 ------ 但Button明明只用到了numhandleClick,和count一点关系都没有啊!

原因:React 默认规则是 "父组件渲染时,所有子组件都会跟着重新渲染"。不管子组件的 props 有没有变化,只要父组件 "动了",子组件就会 "跟着动"。

优化第一招:React.memo 给子组件 "装过滤器"

React.memo的作用是:让子组件只在 props 真正变化时才执行,避免因父组件无关状态变化而 "白干活"。

用法:给子组件 "包一层"

jsx 复制代码
// 用React.memo包裹子组件,实现"props不变则不执行"
const Button = memo(({ num, onClick }) => {
  console.log('Button 执行');
  return <button onClick={onClick}>{num}</button>;
});

效果 :当点击 "count+1" 时,App执行,但ButtonnumonClick都没变,所以Button不会执行(不打印Button 执行)。

原理:对比 props 是否真的变了

React.memo会缓存子组件的执行结果。父组件执行时,它会对比子组件的 "新 props" 和 "旧 props":

  • 如果完全一样,就直接复用上次的结果,不执行子组件;
  • 如果不一样(比如num变了),才让子组件重新执行。

就像快递柜:只有收到新快递(props 变化),才通知你取件(子组件执行);没新快递时,就安安静静的(不执行)。

优化第二招:useCallback 给函数 "保鲜"

用了React.memo后,你可能发现子组件还是瞎渲染 ------ 这大概率是因为 "函数变了"。

问题:函数在父组件中会 "每次重建"

在父组件中,函数每次执行时都会重新创建(内存地址变化)。比如handleClick

jsx 复制代码
// 父组件每次执行,都会创建一个新的handleClick函数
const handleClick = () => {
  console.log('按钮被点击');
};


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

App执行时,handleClick是新创建的(地址变了),导致子组件ButtononClick props 变化 ------React.memo会认为 "props 变了",于是让Button重新执行。

解决:用 useCallback "缓存函数"

useCallback能让函数 "保鲜":只要依赖不变,就不会重新创建,始终保持同一个内存地址。

jsx 复制代码
// 用useCallback缓存函数,依赖为空则永远不重建
const handleClick = useCallback(() => {
  console.log('按钮被点击');
}, [num]); // 依赖数组:只有依赖变化时,函数才重建

效果App执行时,handleClick的地址不变,ButtononClick props 也不变 ------React.memo会阻止Button重新执行。

就像给函数拍了张 "快照",只要条件没变,每次用的都是这张快照,不会重新拍(创建新函数)。

优化第三招:useMemo 给复杂计算 "记笔记"

除了组件 "瞎渲染",重复的复杂计算也是性能杀手。

比如一个需要循环 100 万次的计算函数,明明输入没变,却因为组件重新执行而反复运行 ------ 这就像做数学题时,同样的题目每次都从头算一遍,纯属浪费时间

先看未优化的问题代码:

jsx 复制代码
import { useState } from 'react';

// 复杂计算:模拟耗时操作(循环100万次)
const expensiveComputation = (n) => {
  console.log('开始计算(耗时操作)'); // 计算执行时打印
  for (let i = 0; i < 1000000; i++) {
    // 这里可以是大数据处理、复杂算法等真实耗时逻辑
  }
  return n * 2; // 计算结果:输入值的2倍
};

function App() {
  // 计算相关的状态(会影响计算结果)
  const [num, setNum] = useState(0);
  // 无关状态(仅用于触发组件重新执行)
  const [count, setCount] = useState(0);

  console.log('App组件重新执行'); // 组件执行时打印

  // 未优化:每次组件执行,不管num变没变,都重新计算
  const result = expensiveComputation(num);

  return (
    <div>
      <p>计算结果:{result}</p>
      <button onClick={() => setNum(num + 1)}>num+1(影响计算)</button>
      <button onClick={() => setCount(count + 1)}>count+1(不影响计算)</button>
    </div>
  );
}

运行现象

  • 点击 "num+1":num变化 → App重新执行 → expensiveComputation重新计算(打印 "开始计算")→ 结果更新(合理)。
  • 点击 "count+1":count变化 → App重新执行 → expensiveComputation也重新计算(打印 "开始计算")→ 结果不变(完全没必要的重复计算)。

就像你只是翻了一页书,却把刚才算过的数学题又重新算一遍 ------ 纯属浪费精力。

解决:用 useMemo "缓存计算结果"

jsx 复制代码
  // 优化:用useMemo缓存计算结果,仅当num变化时重新计算
  const result = useMemo(() => {
    // 只有依赖变化时,才会执行这个函数
    return expensiveComputation(num);
  }, [num]); // 依赖数组:仅num变化时触发重新计算

优化后现象

  • 点击 "num+1":num变化 → useMemo检测到依赖变化 → 执行expensiveComputation(打印 "开始计算")→ 结果更新(正常计算)。
  • 点击 "count+1":count变化 → App重新执行,但num没变 → useMemo直接返回缓存的结果 → expensiveComputation不执行(不打印)。

原理:给计算结果 "记笔记"

useMemo就像学生做题时的 "草稿纸":

  • 第一次计算时,把结果记在草稿纸上(缓存);
  • 下次遇到相同的输入(依赖num不变),直接抄草稿纸上的答案,不用重新计算;
  • 只有输入变了(num变化),才擦掉旧答案,重新计算并记下来。

这样就避免了 "相同输入重复计算" 的性能浪费,尤其适合处理大数据、复杂算法等耗时操作。

组件拆分:粒度越细,优化越容易

性能优化的前提是 "组件拆分合理"。如果把所有逻辑堆在一个大组件里,再厉害的 Hook 也救不了你。

为什么拆分有助于优化?

  • 每个组件只负责一小块功能,状态变化时,只有相关组件会执行;
  • 比如把 "用户信息""商品列表""购物车" 拆成独立组件,修改购物车时,用户信息组件就不会跟着瞎执行;
  • 小组件的 props 更简单,React.memo更容易判断 "是否需要执行"(props 少,对比起来快)。

反例:如果用一个大 Context 存所有状态(比如用户、主题、购物车全放一起),任何一个状态变化,所有依赖这个 Context 的组件都会执行 ------ 这就是 "过度共享状态" 导致的性能灾难。

总结:3 个优化工具的适用场景

工具 作用 通俗理解 什么时候用?
React.memo 阻止子组件在 props 不变时执行 子组件的 "过滤器" 子组件纯展示(只依赖 props,无其他状态)
useCallback 缓存函数,避免因函数重建导致子组件执行 函数的 "保鲜盒" 给子组件传递的回调函数(避免函数地址变化)
useMemo 缓存复杂计算结果,避免重复计算 计算结果的 "草稿纸" 有耗时操作(循环、大数据处理)时

记住:优化的核心是 "让组件只做必要的工作"。父组件变化时,无关的子组件要能 "偷懒";计算过的结果,能复用就绝不重复算。

相关推荐
不断努力的根号七1 小时前
qt框架,使用webEngine如何调试前端
开发语言·前端·qt
德育处主任1 小时前
p5.js 线段的用法
javascript·数据可视化·canvas
伍哥的传说2 小时前
React性能优化终极指南:memo、useCallback、useMemo全解析
前端·react.js·性能优化·usecallback·usememo·react.memo·react devtools
JuneXcy2 小时前
leetcode933最近的请求次数
开发语言·javascript·ecmascript
2301_781668619 小时前
前端基础 JS Vue3 Ajax
前端
上单带刀不带妹9 小时前
前端安全问题怎么解决
前端·安全
Fly-ping9 小时前
【前端】JavaScript 的事件循环 (Event Loop)
开发语言·前端·javascript
SunTecTec10 小时前
IDEA 类上方注释 签名
服务器·前端·intellij-idea
在逃的吗喽10 小时前
黑马头条项目详解
前端·javascript·ajax