在 React 开发中,不必要的组件重渲染是前端性能损耗的核心元凶之一。很多时候父组件状态更新,和更新逻辑无关的子组件也会强制重渲染,页面复杂后会造成明显的卡顿、帧率下降。
而 useCallback、useMemo 搭配 React.memo,正是 React 官方提供的缓存式性能优化方案。本文结合真实可运行的实战案例,从零拆解重渲染问题、优化原理、使用场景与避坑指南,看完彻底告别盲目优化。
一、先搞懂核心问题:为什么会无效重渲染?
1.1 原生渲染机制痛点
React 的默认渲染规则非常简单:父组件重新渲染,子组件无条件跟随重新渲染,无论子组件的 props、state 是否发生变化。
就像我们案例中的场景:父组件 App 维护了两个状态 count、name。当我们点击按钮修改 count 计数器时,并未改变子组件依赖的 name 属性,但子组件依然会触发重渲染,造成纯粹的性能浪费。
1.2 基础案例结构说明
我们定义了两个子组件做对比:
- 普通子组件 RegularChild:无任何优化
- 缓存子组件 MemoChild :通过
React.memo包裹缓存
核心组件代码精简片段如下:
javascript
// 普通子组件(无优化)
function RegularChild({name}){
console.log('渲染了RegularChild');
return <h1>{name}</h1>
}
// memo 缓存子组件
const MemoChild = memo(({name})=>{
console.log('MemorizedChild渲染了');
return <div>Hello,{name}</div>
})
// 父组件
function App(){
const [count,setCount] = useState(0);
const [name,setName] = useState('森林')
console.log('App 组件渲染');
return (
<>
<button onClick={()=>setCount(count+1)}>点击计数{count}</button>
<button onClick={()=>setName("峨眉")}>改变名字</button>
<RegularChild name={name} />
<MemoChild name={name}/>
</>
)
}
二、React.memo:组件级缓存的第一道屏障
2.1 memo 优化原理
React.memo 是一个高阶组件(HOC) ,专门用于缓存函数组件。它会对组件的 props 进行浅层比对:
- 如果 props 没有变化 → 复用组件缓存,不重渲染
- 如果 props 发生变化 → 正常重渲染组件
这也是文中注释 memo memorize请记住我 的核心含义:记忆组件的渲染结果,无变化则复用。
2.2 日志结果分析
我们操作页面,观察控制台打印:
- 点击计数按钮(修改 count) :App 组件渲染、RegularChild 渲染、MemoChild 不渲染
- 点击改名按钮(修改 name) :所有组件全部正常渲染
结论 :memo 成功拦截了无关状态更新导致的无效重渲染,只在自身依赖的 props 变化时更新。
三、memo 的致命缺陷:引用类型 props 失效
很多同学以为用了 memo 就万事大吉,实则会遇到经典坑:memo 仅做浅层比对。
如果子组件接收的 props 是 函数、对象、数组 等引用类型,父组件每次渲染都会生成一个全新的引用地址。哪怕函数/内容逻辑完全不变,浅层比对也会判定 props 变更,memo 彻底失效。
3.1 问题复现(新增函数 props)
我们对父组件简单改造,传递一个自定义函数给子组件:
javascript
function App(){
const [count,setCount] = useState(0);
const [name,setName] = useState('森林')
console.log('App 组件渲染');
// 父组件每次渲染都会生成新函数
const changeName = () => {
setName("峨眉")
}
return (
<>
<button onClick={()=>setCount(count+1)}>点击计数{count}</button>
<MemoChild name={name} changeName={changeName}/>
</>
)
}
此时再点击计数按钮、仅修改 count 状态:MemoChild 会重新渲染 。原因就是每次渲染 App,changeName 都是新函数,引用地址不同,memo 比对失效。
四、useCallback:缓存函数,解决引用类型失效问题
4.1 核心作用
useCallback 的唯一目的:缓存函数引用地址。
它会根据依赖项数组,返回一个稳定的函数引用:依赖不变,函数地址永远不变,完美适配 memo 的浅层比对规则。
4.2 实战优化代码
javascript
import { useState, useCallback, memo } from 'react'
function App(){
const [count,setCount] = useState(0);
const [name,setName] = useState('森林')
// 缓存函数:依赖项不变,函数引用不变
const changeName = useCallback(() => {
setName("峨眉")
}, [])
return (
<>
<button onClick={()=>setCount(count+1)}>点击计数{count}</button>
<MemoChild name={name} changeName={changeName}/>
</>
)
}
优化后:修改 count 状态时,changeName 函数引用不变,memo 比对通过,子组件不再无效重渲染。
4.3 useCallback 依赖规则
- 空依赖
[]:组件挂载时生成一次,永久复用 - 添加依赖
[name]:仅 name 变化时,更新函数引用 - 所有函数内部用到的 state、props、上下文,必须写入依赖数组
五、useMemo:缓存计算结果,避免重复计算
聊完函数缓存,再讲数值缓存:useMemo。
在组件中,很多复杂计算(筛选、遍历、格式化、大数据运算)会在每次渲染时重复执行,造成计算性能浪费。useMemo 专门用于缓存计算结果。
5.1 核心作用
- 缓存变量、计算结果、JSX 结构
- 依赖不变,不重复执行计算逻辑,直接复用结果
- 同步执行,渲染阶段完成计算(区别于 useCallback 缓存函数)
5.2 实战场景
javascript
// 复杂计算逻辑,仅 name 变化时重新计算
const formatName = useMemo(() => {
console.log("执行名字格式化计算");
return `景区:${name}`
}, [name])
此时修改 count 状态,不会执行格式化计算,只有 name变更时,才会重新计算,极大优化复杂数据渲染性能。
六、三者搭配黄金组合(核心总结)
日常开发中,memo + useCallback + useMemo 是固定优化搭档:
- React.memo:外层包裹子组件,做组件缓存,拦截无效重渲染
- useCallback :缓存传给子组件的函数 props,稳定函数引用
- useMemo :缓存组件内复杂计算值、对象/数组 props,稳定数值引用、避免重复计算
七、重要避坑指南(新手必看)
7.1 不要无脑优化
缓存本身需要消耗内存和比对性能,简单组件、简单计算不要强行使用。优化收益 < 缓存开销时,反而会降低性能。
7.2 严格遵守依赖规则
useCallback、useMemo 的依赖数组必须完整,缺失依赖会导致缓存数据陈旧,出现 UI 不同步、逻辑 bug。
7.3 useMemo 不要写副作用逻辑
useMemo 是纯计算函数,禁止写定时器、请求、DOM 操作等副作用,副作用统一交给 useEffect。
八、全文总结
- 默认渲染机制:父渲染、子必渲染,存在大量无效重渲染
- memo:组件浅层缓存,解决基本 props 不变的重渲染问题
- useCallback:缓存函数引用,解决 memo 对函数 props 失效的问题
- useMemo:缓存计算结果,避免重复复杂计算,稳定引用类型数据
掌握这三个 API 的组合使用,就能解决 React 项目中 90% 以上的页面渲染性能问题,也是面试高频核心考点。