写 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
明明只用到了num
和handleClick
,和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
执行,但Button
的num
和onClick
都没变,所以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
是新创建的(地址变了),导致子组件Button
的onClick
props 变化 ------React.memo
会认为 "props 变了",于是让Button
重新执行。
解决:用 useCallback "缓存函数"
useCallback
能让函数 "保鲜":只要依赖不变,就不会重新创建,始终保持同一个内存地址。
jsx
// 用useCallback缓存函数,依赖为空则永远不重建
const handleClick = useCallback(() => {
console.log('按钮被点击');
}, [num]); // 依赖数组:只有依赖变化时,函数才重建
效果 :App
执行时,handleClick
的地址不变,Button
的onClick
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 |
缓存复杂计算结果,避免重复计算 | 计算结果的 "草稿纸" | 有耗时操作(循环、大数据处理)时 |
记住:优化的核心是 "让组件只做必要的工作"。父组件变化时,无关的子组件要能 "偷懒";计算过的结果,能复用就绝不重复算。