前言:
在现代 React 开发中,随着应用复杂度的不断提升,性能优化变得越来越重要。React 提供了强大的声明式编程模型,但与此同时,组件频繁的重新渲染和重复计算也可能带来性能瓶颈。为了构建高效、流畅的用户体验,开发者需要掌握 React 提供的性能优化工具。
在众多优化手段中,React.memo、useCallback 和 useMemo 是最常用也是最核心的三个“性能优化 Hook”。它们分别从组件渲染控制、函数引用缓存和计算结果记忆化三个层面,帮助我们减少不必要的渲染和计算,从而提升应用性能。
一、React 组件的渲染顺序
- 首次渲染:从父组件 → 子组件(从外到内)依次渲染。
- 更新渲染:当状态更新后,React 会重新渲染组件树,默认情况下父组件更新会触发子组件重新渲染,即使子组件的 props 没有变化。
React 组件在每次状态更新时都会重新执行整个函数体(即函数组件的 body)。
举个例子:
App.jsx
import {
useState,
useEffect,
} from 'react'
import './App.css'
import Button from './components/Button'
function App() {
const [count, setCount] = useState(0)
console.log('App render')
useEffect(() => {
console.log(count)
},[count])
return (
<>
<Button>Click me</Button>
</>
)
}
export default App
Button.jsx
import {
useEffect,
} from 'react'
const Button = () => {
useEffect(() => {
console.log('Button useEffect')
},[])
console.log('Button render')
return <button>Click me</button>
}
export default Button
效果:
⚠️ 问题:如果子组件是纯组件(即 props 不变时渲染结果不变),但每次父组件更新时仍然重新渲染,就会造成不必要的性能开销。
二、React.memo:避免子组件不必要的渲染
用memo包裹子组件:
import {
useEffect,
memo, // 阻止子组件的重新渲染
} from 'react'
const Button = () => {
// const Button = () => {
useEffect(() => {
console.log('Button useEffect')
},[])
console.log('Button render')
return <button>Click me</button>
}
// 高阶组件
export default memo(Button)
三、useCallback:缓存回调函数,防止子组件重复渲染
1. 什么是 useCallback?
useCallback 是一个 Hook,用于缓存一个函数引用。它会在依赖项没有变化时返回同一个函数引用,从而避免因为函数引用变化而导致子组件重新渲染。
2. 使用方式:
import { useCallback } from 'react';
const handleClick = useCallback(() => {
console.log('Clicked');
}, [依赖项]);
还是前面的例子:
import {
useState,
useEffect,
} from 'react'
import './App.css'
import Button from './components/Button'
function App() {
const [count, setCount] = useState(0)
console.log('App render')
useEffect(() => {
console.log(count)
},[count])
// 新增的函数
const handleClick = () => {
console.log('handleClick')
}
return (
<>
<Button onClick={handleClick}>Click me</Button>
</>
)
}
export default App
我们发现就算在子组件加了memo,当父组件状态更新,会重新渲染,而父组件传给子组件的回调函数handleClick也会跟着重新渲染,这是不必要的。此时可以用useCallback来优化。
我们添加一个状态
num,用于模拟不变的依赖项。
// 用useCallback
const handleClick = useCallback(() => {
console.log('handleClick')
},[num])
这时,修改count并不会引发回调函数handleClick的重新渲染。
四、React.memo 和 useCallback 总结
父组件重新渲染 → 子组件即使 props 没变也重新渲染 → 用
React.memo阻止但如果传入的 props 是一个函数,函数每次重新生成 →
React.memo无效 → 用useCallback缓存函数
五、useMemo:缓存计算结果,避免重复计算
1. 什么是 useMemo?
useMemo 是一个 Hook,用于缓存某个计算结果,避免每次渲染都重复计算,适用于复杂计算但依赖项变化不频繁的情况。
2. 使用方式:
const result = useMemo(() => {
return expensiveComputation(count);
}, [count]);
3. 使用场景:
- 计算量大,但依赖项变化不频繁。
- 与
React.memo配合使用,避免传递的值频繁变化导致子组件重新渲染。
const expensiveComputation = (n) => {
console.log('expensiveComputation')
// 复杂计算,开销高
for (let i = 0; i < 1000000000; i++) {
// 模拟复杂计算
i++
}
return n * 2
}
const result = useMemo(() => {
return expensiveComputation(count)
}, [count])
useMemo 缓存了 expensiveComputation(count) 的执行结果。
- 只有当
count变化时,才会重新执行这个耗时的计算。 - 如果
count没变,就直接返回上一次计算的结果,跳过重复计算,提升性能。
4.useMemo 原理简述
useMemo 的语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
- 第一个参数是一个“计算函数”,它会在需要的时候执行。
- 第二个参数是依赖项数组。
- 只有当依赖项发生变化时,才会重新执行计算函数。
- 否则返回上一次缓存的结果。
5.为什么要用 useMemo?
避免重复的昂贵计算
expensiveComputation 函数模拟了一个非常耗时的操作(10 亿次循环),如果每次组件渲染都执行一次,会严重影响性能。
使用 useMemo 后:
- 只有当
count改变时才执行一次计算。 - 其他时候直接返回缓存结果。
⚠️ 不要用来“优化渲染”
useMemo是优化计算,不是优化渲染。如果希望优化组件渲染,应该用React.memo。
六、总结对比
| Hook / API | 作用 | 场景 | 注意事项 |
|---|---|---|---|
React.memo | 缓存组件渲染,避免重复渲染 | 子组件 props 没有变化但频繁渲染 | 仅浅比较,对象/数组需保持引用一致 |
useCallback | 缓存函数引用 | 传递给子组件的回调函数 | 依赖项必须正确,否则闭包问题 |
useMemo | 缓存计算结果 | 复杂计算、避免重复执行 | 不要滥用,比较也有开销 |