第 07 课 · 性能优化三件套
🎯 本课目标
- 说清「父组件渲染 → 子组件默认跟着渲染」这条 React 心智模型,并解释它与 Vue 自动依赖追踪的本质差异
- 会用
React.memo跳过「props 没变」的子组件渲染,知道浅比较能比什么、比不了什么 - 会用
useCallback/useMemo稳定引用,避免内联对象、内联函数把 memo 变成摆设 - 能用「渲染计数 + 控制台日志 + Profiler」定位多余渲染,并用三个问题判断到底值不值得优化
- 记住优化顺序:结构性优化(状态下沉、children 透传、组件拆分、key)永远排在包 memo 前面
1. 心智前提:React 没有「自动依赖追踪」
这是 Vue 转 React 最大的一处认知差,先把它掰直,后面三个 API 全是这条前提的推论。
Vue 里你这样写
<script setup lang="ts">
import { ref } from 'vue'
import ExpensiveList from './ExpensiveList.vue'
const count = ref(0)
</script>
<template>
<!-- count 变了,ExpensiveList 一个字都不会重新渲染 -->
<ExpensiveList :items="items" />
<button @click="count++">{{ count }}</button>
</template>
count 变化时,Vue 精确地知道模板里只有那一段文本用了 count(编译期就收集好了依赖),于是只更新那一段,子组件完全不参与。要不要更新,是 Vue 替你算好的。
React 里这样写
import { useState } from 'react'
import ExpensiveList from './ExpensiveList'
function Parent() {
const [count, setCount] = useState(0)
return (
<>
{/* props 一个字没变,但 Child 的函数体依然会完整执行一次 */}
<ExpensiveList items={items} />
<button onClick={() => setCount(count + 1)}>{count}</button>
</>
)
}
点击按钮后发生的事:setCount → Parent 函数整体重新执行 → 返回一棵全新的 JSX 树 → React 拿新树和旧树 diff → 更新真正变化的 DOM。注意第二步:所有子组件都会被重新执行,React 不会先帮你判断「这个子组件的输出会不会变」,它默认全量渲染,把「要不要跳过」的决定权交给你。
关键差异与注意点
- Vue 是 push 模型:依赖变了,精准通知用到它的地方;React 是 pull 模型:状态变了,从该组件往下默认全跑一遍。
- React 的「渲染」≠「更新 DOM」:组件函数重新执行 + 创建 JSX 是开销所在,diff 之后 DOM 大多没变化。所以「多余渲染」浪费的是 JS 执行时间,不是页面闪烁。
- 好消息:中小型业务页默认全量渲染根本不慢,绝大多数组件不需要任何优化。要优化的只有三类:昂贵计算、大列表、被高频更新父组件带着跑的子树。
- Vue 里你几乎从没写过「性能优化」代码;React 里这是 code review 的必查项——因为框架不替你做。
2. React.memo:给子组件加一道浅比较闸门
Vue 里你这样写
Vue 没有这个概念。硬要找对应物,是 shallowRef / v-memo 这类手动优化手段,但业务代码里极少用。
React 里这样写
import { memo } from 'react'
interface UserRowProps {
name: string
dept: string
onEdit: (id: number) => void
}
// memo(UserRow) 返回一个带浅比较的新组件
const UserRow = memo(function UserRow({ name, dept, onEdit }: UserRowProps) {
console.log('UserRow 渲染', name) // 上线前删掉,开发期用来观察
return (
<tr>
<td>{name}</td>
<td>{dept}</td>
<td>
<button onClick={() => onEdit(1)}>编辑</button>
</td>
</tr>
)
})
// 想完全自定义比较逻辑时才写第二个参数(返回 true 表示「跳过渲染」)
const UserRow2 = memo(
UserRow,
(prev, next) => prev.name === next.name && prev.dept === next.dept,
)
关键差异与注意点
memo对每个 props 逐个做Object.is比较(浅比较):原始值比「值」,对象 / 数组 / 函数比「引用」。- 所以 memo 的效果完全取决于 props 的引用是否稳定——这就引出下一节,也是 90% 的 bug 来源。
- 第二个参数(自定义比较函数)企业里很少用:它让组件的正确性依赖调用者记住「哪些 props 影响显示」,漏比一个就是「改了数据不更新」的事故。默认浅比较 + 稳定引用是更安全的方案。
- memo 不是免费的:比较本身有成本,且包裹后组件层级多一层。只包「确认在多余渲染、且渲染成本不低」的组件。
3. 引用稳定性:useCallback 与 useMemo
Vue 里你这样写
<template>
<!-- 内联箭头函数、内联对象在 Vue 里完全无所谓,没有引用比较这回事 -->
<UserRow :user="{ name, dept }" @edit="() => edit(user.id)" />
</template>
React 里这样写
上面这种写法在 React 里会让 memo 完全失效——每次渲染 props.user 和 props.onEdit 都是新的引用。正确写法:
import { memo, useCallback, useMemo, useState } from 'react'
const UserRow = memo(function UserRow({ user, onEdit }: { user: User; onEdit: (id: number) => void }) {
/* ... */
})
function UserTable() {
const [keyword, setKeyword] = useState('')
// useCallback:依赖不变,函数引用不变(锁「函数」)
const handleEdit = useCallback((id: number) => {
console.log('编辑', id)
}, [])
// useMemo:依赖不变,计算结果引用不变(锁「对象 / 数组 / 计算结果」)
const filtered = useMemo(
() => users.filter((u) => u.name.includes(keyword)),
[keyword],
)
return filtered.map((u) => <UserRow key={u.id} user={u} onEdit={handleEdit} />)
}
一句话分工:useCallback 锁函数,useMemo 锁对象 / 数组 / 任何计算结果。useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。
关键差异与注意点
- 依赖数组的语义:依赖里的任何一个值变了就重新创建(重新计算 / 重新生成函数)。它像
watch的watchSources,但没有自动追踪——漏写依赖就会一直用旧值,这是最常见的 bug。 - 内联对象、内联数组、内联箭头函数每次渲染都是新引用:
style={{ color: 'red' }}、items={[1, 2]}、onClick={() => ...}都是。传给 memo 组件或作为其他 Hook 的依赖时,必须先锁引用。 setState函数本身就是稳定引用(React 保证),可以直接传给子组件,不需要再包一层useCallback。
4. useMemo 的两大用途,对照 computed
Vue 里你这样写
const filtered = computed(() => users.value.filter((u) => u.name.includes(keyword.value)))
computed 一个语法同时给你两个能力:缓存计算结果(依赖不变不重算)+ 引用稳定(依赖不变返回同一个值)。你不用关心它是哪种优化。
React 里这样写
useMemo 的两个用途要分开理解,因为它们适用的场景不同:
// 用途一:跳过昂贵计算(数据量大 / 计算复杂时才值得)
const sortedUsers = useMemo(() => {
return users.filter((u) => u.active).sort((a, b) => b.score - a.score)
}, [users])
// 用途二:稳定引用,让 memo 子组件 / 其他 Hook 的依赖不失效
const chartData = useMemo(() => ({ labels, values }), [labels, values])
useEffect(() => {
drawChart(chartData)
}, [chartData]) // chartData 引用不变时,这个 effect 不会重复执行
关键差异与注意点
- Vue 的
computed自动兼得两个用途;React 必须手动写依赖数组,漏写依赖 = 拿到旧结果(对应 Vue 里 watch 忘加依赖,但这里更容易踩)。 - 昂贵的判断标准:1 万条以上的过滤 / 排序 / 聚合、复杂图表数据处理、多层嵌套展开。
a + b、list.length这类直接写表达式,别包。 useMemo只是「提示」不是保证:React 可能在内存压力下丢弃缓存重算,所以计算必须保持纯净(不能在里面发请求、改外部变量)。
5. 企业判断标准:先量化,再优化
优化三问(code review 时按顺序问)
- 谁在多余渲染? 先加渲染日志(
console.log在组件体里)或用 React DevTools Profiler 录一段,确认真的存在多余渲染。没有数据就动手优化 = 猜。 - props 是什么类型? 全是字符串 / 数字 →
memo直接生效;有对象 / 函数 → 先解决引用稳定性。 - 值不值得? 渲染成本高(大列表行、复杂图表、几百行表格)才值得包;一个渲染 3 行的卡片包了 memo 只是增加噪音。
定位工具
- 渲染日志:组件体里
console.log('Xxx 渲染'),最土最快。 - 渲染计数:
useRef在渲染体内自增,页面上直接显示次数。 - React DevTools Profiler:点一下「record」,操作页面,能直接看到「这次提交谁渲染了、为什么渲染(props 变了 / 父组件渲染了)」,是团队里最常用的证据来源。
AI 生成代码的通病:处处 useCallback
AI 生成的 React 代码几乎会把每个函数都包上 useCallback、每个表达式都包上 useMemo。review 时按这个标准删:
- 该删:传给普通 DOM 元素的事件(
<button onClick={fn}>不需要稳定引用);组件内部自产自销、没传给 memo 子组件 / 没进依赖数组的值;依赖项每次都变的(锁了也白锁,如依赖keyword每次输入都变)。 - 该留:传给
memo子组件的回调;作为useEffect/useMemo依赖的自定义函数;被自定义 Hook 对外暴露的函数。
6. 结构性优化优先于包 memo
「先改结构,再谈 memo」:
- 状态下沉:把高频变化的状态放到真正需要它的最小组件里,而不是全塞在页面顶部让整页跟着渲染。对应 Vue 里「别把所有 ref 都写在父组件」。
- children 透传:会更新的组件把不变的内容作为
children/ JSX prop 接收——children引用不变时整棵子树跳过渲染。 - 组件拆分:一个大组件里拆出「高频区」和「静态区」,让高频状态只困在高频小组件里。
- 列表 key:稳定且唯一的
key(业务 id),让 diff 走复用而不是重建;用数组下标当 key,在有插入 / 删除的列表里会导致状态错乱 + 大量多余渲染,和 Vue 的规则完全一致。
📊 Vue ↔ React 对照速查表
| 你熟悉的(Vue 3) | React 里叫 | 一句话差异 |
|---|---|---|
| 自动依赖追踪(编译期收集) | 无,默认子组件跟着父渲染 | React 要手动 memo 声明「可跳过」 |
computed | 直接表达式 / useMemo | computed 自动缓存 + 自动依赖;React 手写依赖数组 |
shallowRef | React.memo(浅比较 props) | 都是「只比第一层引用」的思路 |
v-memo / v-once | useMemo + memo | 手动标记「这段可以不算 / 不渲染」 |
| slot 内容 | children / JSX props | 两者都能让宿主更新不带上插槽内容 |
:key 列表复用 | key | 同样禁止用数组下标当 key(有增删时) |
| watch 依赖数组 | Hook 依赖数组 | React 不自动追踪,漏写 = 拿旧值 |
| Vue Devtools 组件高亮 | React DevTools Profiler | 定位多余渲染的主流工具 |
| 模板里内联函数 / 对象 | useCallback / useMemo | React 里内联 = 新引用,会让 memo 失效 |
defineProps 响应式 props | 每次渲染重新传入的 props | React 的 props 是「本次渲染的快照」 |
🏢 企业实战要点
- 命名规范:被
memo包裹的组件保持原名字(memo(function UserRow(){})而不是memo(function _Row(){})),DevTools 里才好认;useMemo返回值用名词命名(filteredList),useCallback的函数用动词开头(handleEdit)。 - 文件拆分:memo 组件定义在使用方旁边或单独文件,不要在一个文件里 memo 十几个小组件——可读性崩塌。
- 列表页标配(中后台最常见的组合):表格行
memo+ 行级回调useCallback+ 稳定业务 id 做key+ 筛选条件useMemo。这一套组合是面试和实际工作的高频场景。 - React.memo 第二参数少用:自定义比较函数容易漏比 props,review 里默认要求删掉;除非能写出完整注释说明为什么浅比较不够。
- 真实坑 1:
memo子组件怎么都不更新 → 十有八九是父组件传了内联对象 / 内联函数,引用永远不同。先查 props 再怀疑 memo。 - 真实坑 2:
useMemo漏写依赖导致「筛选结果不刷新」→ 现象和 Vue 里 watch 漏依赖一模一样,但 React 不报任何警告(装eslint-plugin-react-hooks才有提示)。 - 真实坑 3:为了「性能」把所有状态塞进 Redux/Zustand 全局,结果任何一处更新全应用渲染 → 先做状态下沉,再谈全局状态。
- 评审话术:「这个组件渲染成本多少测过吗?」「这里的对象引用稳定吗?」「不包 memo 会慢吗?」——三句话覆盖本课全部内容。
💼 面试高频
Q1:memo、useMemo、useCallback 的区别与联系?
memo 是高阶组件,作用于组件,浅比较 props 决定要不要跳过渲染;useMemo 缓存任意计算结果,依赖不变返回旧值;useCallback 是 useMemo 的函数特例,专门缓存函数引用。联系在后两者:memo 的浅比较遇到对象 / 函数 props 时必须靠它们稳定引用,三者经常一起出现,但 memo 也可以单独用(纯原始值 props 时)。
Q2:为什么需要 useCallback?直接传函数不行吗?
不行,前提是这个组件被 memo 过或者这个函数是其他 Hook 的依赖。React 每次渲染都会重新执行组件函数,函数体里定义的箭头函数每次都是新的引用;memo 子组件浅比较发现「onEdit 变了」就会渲染,memo 等于白包。useCallback 用依赖数组控制「什么时候才生成新函数」,从而让引用稳定。
Q3:Vue 和 React 的渲染优化机制差异?(高频)
Vue 编译期收集依赖、运行期精准 push 更新,子组件不依赖的数据变化不会触发它渲染,开发者几乎零成本;React 是状态变化 → 组件函数整体重渲染 → 递归向下,靠开发者手动用 memo / useMemo / useCallback 声明跳过条件。所以 Vue 的优化是「默认就好」,React 的优化是「默认全量、手动剪枝」,这也决定了 React 项目里性能优化是常规 code review 项。
Q4:线上发现页面卡,怎么定位多余渲染?
先复现 + 录 React DevTools Profiler:看火焰图里哪个组件重复提交、why 渲染原因是什么(props 变化 / 父组件渲染 / 自身 state)。再按「三问」处理:确认是多余渲染 → 看 props 类型解决引用稳定 → 判断值不值得(大列表、重计算才包),同时检查 key、状态下沉这些结构性问题。最后用 Profiler 复测,证明优化有效。
Q5:useMemo 什么情况下不该用?
计算本身廉价(几个字段拼接、数组长度)、结果每次都必须最新(依赖高频变化,缓存命中率极低)、计算有副作用(请求、改外部变量)这三种都不该用。滥用会让代码变长、依赖数组变成维护负担,review 里这是明确的减分项。
📌 小结 + 下节预告
- React 默认「父渲染、子跟渲染」,没有 Vue 那种自动依赖追踪——这是所有性能问题的根源。
memo是子组件的浅比较闸门;useCallback锁函数、useMemo锁对象和昂贵计算;内联引用类型 props 会让前功尽弃。- 企业标准动作:先量化(日志 / Profiler)→ 看引用稳定性 → 判断值不值得;结构性优化永远优先。
下一课(第 08 课 · 自定义 Hook):当你在两个组件里写了几乎一样的 useState + useEffect,就该抽自定义 Hook 了——它就是 Vue 里 composable 的 React 写法。
本文系统讲解React性能优化核心机制:React.memo用于跳过未变化props的子组件渲染;useCallback稳定函数引用,避免memo失效;useMemo缓存计算结果并保证对象引用稳定。强调React默认全量渲染与Vue自动依赖追踪的本质差异,指出优化前提为量化定位、引用稳定性及结构性优化优先。企业实践中需结合Profiler、渲染日志等工具,规避AI生成代码中滥用useCallback/useMemo的常见问题。
1902

被折叠的 条评论
为什么被折叠?



