React07:性能优化三件套

本文系统讲解React性能优化核心机制:React.memo用于跳过未变化props的子组件渲染;useCallback稳定函数引用,避免memo失效;useMemo缓存计算结果并保证对象引用稳定。强调React默认全量渲染与Vue自动依赖追踪的本质差异,指出优化前提为量化定位、引用稳定性及结构性优化优先。企业实践中需结合Profiler、渲染日志等工具,规避AI生成代码中滥用useCallback/useMemo的常见问题。

第 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 时按顺序问)

  1. 谁在多余渲染? 先加渲染日志(console.log 在组件体里)或用 React DevTools Profiler 录一段,确认真的存在多余渲染。没有数据就动手优化 = 猜。
  2. props 是什么类型? 全是字符串 / 数字 → memo 直接生效;有对象 / 函数 → 先解决引用稳定性。
  3. 值不值得? 渲染成本高(大列表行、复杂图表、几百行表格)才值得包;一个渲染 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直接表达式 / useMemocomputed 自动缓存 + 自动依赖;React 手写依赖数组
shallowRefReact.memo(浅比较 props)都是「只比第一层引用」的思路
v-memo / v-onceuseMemo + memo手动标记「这段可以不算 / 不渲染」
slot 内容children / JSX props两者都能让宿主更新不带上插槽内容
:key 列表复用key同样禁止用数组下标当 key(有增删时)
watch 依赖数组Hook 依赖数组React 不自动追踪,漏写 = 拿旧值
Vue Devtools 组件高亮React DevTools Profiler定位多余渲染的主流工具
模板里内联函数 / 对象useCallback / useMemoReact 里内联 = 新引用,会让 memo 失效
defineProps 响应式 props每次渲染重新传入的 propsReact 的 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 写法。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值