React memo、useCallback 与 useMemo:父组件多状态时如何减少无效渲染

React memo、useCallback 与 useMemo:父组件多状态时如何减少无效渲染

标签:React, memo, useCallback, useMemo

父组件同时维护 countname 时,点击计数按钮会让父组件重新渲染。子组件只依赖 name,是否也必须重新执行?答案是:可以用 memo 根据 Props 做跳过判断;如果传递的是函数,还要考虑 useCallback 的引用稳定;如果问题是昂贵计算,则使用 useMemo 缓存结果。本文基于本地 React + Vite Demo 静态整理,运行未验证

当前 Demo 实际演示了什么

当前源码的关键结构:

jsx 复制代码
const RegularChild = memo(function RegularChild({ name }) {
  console.log('RegularChild 组件渲染')
  return <h1>当前名字:{name}</h1>
})

function App() {
  const [count, setCount] = useState(0)
  const [name, setName] = useState('少林队')

  return (
    <>
      <button onClick={() => setCount(count + 1)}>增加</button>
      <button onClick={() => setName('峨眉队')}>改变名字</button>
      <RegularChild name={name} />
    </>
  )
}

这里要先澄清:目录叫 useCallback,但当前 App.jsx 实际导入和使用的是 useStatememo,没有真正调用 useCallbackuseMemo

父组件状态变化为什么会牵连子组件

text 复制代码
点击增加
  ↓
count 改变
  ↓
App 重新执行
  ↓
RegularChild 收到 name
  ↓
memo 比较 Props

没有 memo 时,父组件重新渲染,子组件通常也会跟随执行。这里 RegularChild 只接收 name,不依赖 count,因此计数变化并不需要它重新计算名字标题。

memo 如何减少子组件渲染

jsx 复制代码
const RegularChild = memo(function RegularChild({ name }) {
  return <h1>当前名字:{name}</h1>
})

memo 默认对 Props 做浅层比较:

text 复制代码
name 没变化 → 尝试跳过子组件重新执行
name 变化   → 子组件重新执行

当前 name 是字符串,比较比较直接。点击"增加"时,count 变化但 name 不变,RegularChild 理论上可以跳过;点击"改变名字"时,name 从"少林队"变为"峨眉队",子组件需要更新。

需要注意,memo 只影响被包装的子组件,不会阻止 App 自己重新渲染,也不是业务正确性的保证。

useCallback 解决的是函数引用

如果子组件接收函数:

jsx 复制代码
const handleSelect = () => {
  console.log(name)
}

<RegularChild onSelect={handleSelect} />

即使函数逻辑看起来没有变化,父组件每次重新渲染也可能创建一个新的函数对象:

text 复制代码
第一次渲染:handleSelect 引用 A
第二次渲染:handleSelect 引用 B

配合 memo 时,子组件可能认为 onSelect 这个 Prop 发生了变化。此时可以使用:

jsx 复制代码
const handleSelect = useCallback(() => {
  console.log(name)
}, [name])

依赖的 name 不变时,React 可以复用函数引用;name 变化时,重新创建函数,以便函数读取最新的 name

一句话:

text 复制代码
memo:比较子组件 Props,决定是否跳过
useCallback:缓存传给子组件的函数引用

useMemo 缓存计算结果

useMemo 解决的是计算结果重复生成的问题:

jsx 复制代码
const completedItems = useMemo(() => {
  return items.filter(item => item.done)
}, [items])

含义是:

  • todos 没变化:复用上一次计算结果
  • todos 变化:重新执行过滤逻辑

对比:

API 缓存对象 典型场景
memo 子组件渲染结果的跳过判断 子组件 Props 未变化
useCallback 函数引用 函数作为 Props 传递
useMemo 计算结果 昂贵计算或需要稳定对象引用

不要把优化 Hook 当成默认写法

记忆化也有成本:需要维护依赖数组和比较缓存。小组件、简单计算、没有性能瓶颈时,直接写普通代码通常更清晰。

建议顺序:

text 复制代码
先确认组件或计算确实重复且昂贵
  ↓
用日志或 Profiler 定位
  ↓
再选择 memo、useCallback 或 useMemo
  ↓
验证优化前后的渲染行为

源码排查提醒

当前 main.jsx 导入了 StrictMode,但没有使用它包裹 App

jsx 复制代码
createRoot(document.getElementById('root')).render(
  <App />
)

因此,如果控制台出现重复日志,不能仅凭这份源码直接归因于 StrictMode;本次项目也没有运行验证,具体原因需要结合实际入口、开发工具和运行环境检查。

性能优化自检清单

  • 子组件是否真的因为父组件无关状态变化而重复执行?
  • 子组件 Props 是否包含每次都会新建的对象、数组或函数?
  • 如果传函数,是否需要 useCallback 稳定引用?
  • 如果有复杂计算,是否需要 useMemo 缓存结果?
  • 是否确认依赖数组包含了计算或函数使用的外部值?
  • 是否用 Profiler 或日志验证优化确实生效?
  • 是否因为过度优化增加了代码复杂度?

总结

React 性能优化不是看到重新渲染就全部阻止,而是先判断更新是否有必要。当前 Demo 中,memoRegularChild 可以关注 name 而忽略 count;函数 Props 场景再考虑 useCallback,昂贵计算场景再考虑 useMemo。最终可以记住:memo 管子组件跳过,useCallback 管函数引用,useMemo 管计算结果。本文代码运行未验证。

相关推荐
朱涛的自习室2 小时前
从 Prompt 到 Graph:AI 工程的进化史
android·前端·人工智能
今天的砖头有点烫手啊3 小时前
AI Agent 开发实战(十):Agent 设计模式(ReAct / Plan-Execute / Reflection)
人工智能·react.js·设计模式
敲代码的玉米C3 小时前
让两个模型一写一审
前端·人工智能·架构
代码不加糖3 小时前
common.js和es6中模块引l入的区别?
前端·javascript·es6
qq83443103 小时前
一款纯前端的excel控件,XuY_Sheet 电子表格组件教程
前端·excel·教程·luckysheet·xuy_sheet控件·前端表格控件
云浪3 小时前
从 0 到实战:掌握向量数据库 Milvus,构建 AI 应用的核心能力
javascript·数据库·人工智能
不爱说话郭德纲4 小时前
零基础,学做KMP项目,TRAE Work手把手带你月薪.....
前端·后端·app
敲代码的玉米C4 小时前
Agent 做 IDE
前端·人工智能·开源
鹏北海4 小时前
AI 全栈时代的多语言 SDK 版本管理:认识 mise
前端·后端