React 性能优化三剑客:React.memo、useCallback 与 useMemo 深度解析

306 阅读4分钟

前言:

在现代 React 开发中,随着应用复杂度的不断提升,性能优化变得越来越重要。React 提供了强大的声明式编程模型,但与此同时,组件频繁的重新渲染和重复计算也可能带来性能瓶颈。为了构建高效、流畅的用户体验,开发者需要掌握 React 提供的性能优化工具。

在众多优化手段中,React.memouseCallbackuseMemo 是最常用也是最核心的三个“性能优化 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

效果:

image.png

⚠️ 问题:如果子组件是纯组件(即 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)

image.png

三、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来优化。

4f8c71ec926f51b56931c2f4d4a165ae.png 我们添加一个状态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缓存计算结果复杂计算、避免重复执行不要滥用,比较也有开销