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 管计算结果。本文代码运行未验证。

相关推荐
FreeTinker6 小时前
HTML本地存储技术解析:localStorage与sessionStorage的原理、差异与应用场景
前端·html
qq_452396236 小时前
第十二篇:《全链路监控与可观测性:前端错误追踪与性能分析》
前端
海兰6 小时前
【应用】基于 Next.js 16 + Python mplfinance的金融K线图与技术指标可视化平台(三)
javascript·python·金融
wangruofeng7 小时前
Phosphor Icons 官网源码拆解:URL 即存储、水波动画与 7362 Star 图标库的架构实践
前端·架构·github
BigTopOne7 小时前
WebRTC Native / Android 开发学习资源整理
前端
excel8 小时前
nuxt 3 升级 nuxt 4 报错之 hasInjectionContext
前端
小磊哥er8 小时前
深入解构Claude Code - 第 7 篇 · 连接外面的世界
javascript·ai编程
何以解忧,唯有..8 小时前
LangChain 工具调用(Tool Calling)实战指南
java·前端·langchain
小磊哥er8 小时前
深入解构Claude Code - 第 6 篇 · 终端里的图形界面
javascript·ai编程