Skip to content

React组件渲染性能优化

约 2747 字大约 9 分钟

react

2025-04-05

在大型 React 应用中,组件渲染性能直接影响用户体验。

渲染机制与优化

当父组件状态变化时,React 默认会重新渲染该组件及其所有子组件,即使子组件的 props 未改变。

这种机制可能导致不必要的渲染开销:

因此,优化核心策略是 阻断不必要的渲染链,我们可以通过 浅比较(shallow comparison) 来实现这一目标。

类组件渲染性能优化

注意

shouldComponentUpdatePureComponent 主要用于 类组件,虽然 React 更推荐使用函数式组件, 但了解其渲染优化策略是有必要的。 不仅仅是因为对于维护旧的类组件代码有帮助,而且在优化策略上是相通的。

我们先来看一个简单的 类组件 例子:

class Child extends React.Component {
  render() {
    console.log('Child rendered')
    return (
      <div>
        <h1>
          Child Count:
          {this.props.count}
        </h1>
      </div>
    )
  }
}

class App extends React.Component {
  state = {
    count: 0,
    otherValue: 'Hello',
  }

  increment = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }))
  }

  changeOtherValue = () => {
    this.setState(prevState => ({ otherValue: prevState.otherValue === 'Hello' ? 'World' : 'Hello' }))
  }

  render() {
    console.log('Parent rendered')
    return (
      <div>
        <h1>
          otherValue:
          {this.state.otherValue}
        </h1>
        <Child count={this.state.count} />
        <button onClick={this.increment}>Increment Count</button>
        <button onClick={this.changeOtherValue}>Change Other Value</button>
      </div>
    )
  }
}

export default App

在这个应用中,Child 组件的 Props countAppState 的一部分。

当我们点击 Increment Count 按钮时,Appcount 会增加, 此时,App 会重新渲染,Child 也会重新渲染。

render-1

当我们点击 Change Other Value 按钮时,otherValue 发生了改变,App 会重新渲染。 虽然 Child 组件没有用到 otherValue, 但 Child 依然重新渲染了。

render-2

这是因为当 Parent 组件的 stateprops 发生变化时,React 会默认重新渲染该组件及其所有 Child 组件。

在这种情况下,我们可以通过 shouldComponentUpdate 来进行优化,来阻断不必要的渲染。

shouldComponentUpdate

React -> Component -> shouldComponentUpdate

shouldComponentUpdate 是 类组件 的生命周期。它用于决定组件是否需要更新。当返回 true 时,表示组件可以更新, 返回 false 时,表示组件不需要更新。

我们对 Child 组件进行优化:

class Child extends React.Component {
  shouldComponentUpdate(nextProps) {
    // 仅当count变化时更新
    return this.props.count !== nextProps.count
  }

  render() {
    console.log('Child rendered')
    // ...
  }
}

我们重新分别点击 Increment CountChange Other Value 按钮,可以看到:

render

PureComponent

React -> PureComponent

除了使用 shouldComponentUpdate 进行优化外,React 还提供了一个更加简便的方法:使用 PureComponent

PureComponent 类似于 Component,但是当 propsstate 与之前保持一致时会跳过重新渲染。

我们使用 PureComponent 重写 Child 组件:

class Child extends React.PureComponent {
  render() {
    console.log('Child rendered')
    return (
      <div>
        <h1>
          Child Count:
          {this.props.count}
        </h1>
      </div>
    )
  }
}

count 一致时,Child 会跳过重新渲染。

需要注意的是,PureComponent 只对 PropsState 进行浅比较, 如果 props 或 state 中包含复杂的数据结构(如对象或数组),浅比较可能无法正确判断数据是否发生变化。 在这种情况下,可以使用深比较或手动实现 shouldComponentUpdate 来确保组件正确地更新。

浅比较的注意事项

重要提示

当 props 包含嵌套对象时,需返回新引用(如 [...list]{...obj})才能触发更新。

函数组件渲染性能优化

我们来看下面的 函数组件 例子:

这是 React 推荐的函数组件写法

它实现了与上面的 类组件实现的 应用完全相同的功能。

import { useState } from 'react'

function Child({ count }) {
  console.log('Child rendered')
  return (
    <div>
      <h1>
        Child Count:
        {count}
      </h1>
    </div>
  )
}

function App() {
  const [count, setCount] = useState(0)
  const [otherValue, setOtherValue] = useState('Hello')

  const increment = () => {
    setCount(prevCount => prevCount + 1)
  }

  const changeOtherValue = () => {
    setOtherValue(prevValue => prevValue === 'Hello' ? 'World' : 'Hello')
  }

  console.log('Parent rendered')
  return (
    <div>
      <h1>
        otherValue:
        {otherValue}
      </h1>
      <Child count={count} />
      <button onClick={increment}>Increment Count</button>
      <button onClick={changeOtherValue}>Change Other Value</button>
    </div>
  )
}

export default App

当我们点击 Increment Count 按钮时,Appcount 会增加, 此时,App 会重新渲染,Child 也会重新渲染。

render-1

当我们点击 Change Other Value 按钮时,otherValue 发生了改变,App 会重新渲染。 虽然 Child 组件没有用到 otherValue, 但 Child 依然重新渲染了。

render-2

React.memo

React -> memo

React.memo 是一个高阶组件,使用 memo 将组件包装起来,以获得该组件的一个 记忆化 版本。 通常情况下,只要该组件的 props 没有改变,这个记忆化版本就不会在其父组件重新渲染时重新渲染。

我们使用 React.memo 重写 Child 组件:

const Child = React.memo(({ count }) => {
  console.log('Child rendered')
  return (
    <div>
      <h1>
        Child Count:
        {count}
      </h1>
    </div>
  )
})

我们重新分别点击 Increment CountChange Other Value 按钮,可以看到:

render

可以把 React.memo 看做是 PureComponent 的等价实现

Rect.memo 只对 props 进行浅比较

对于 props 为比较复杂的数据结构时,我们可以在需要更新时返回一个新的对象或数组,以避免浅比较的问题。

还可以使用 React.memo 的第二个参数,自定义 props 的比较逻辑。

React.memo 语法

const Child = React.memo(
  props => <div>{/* ... */}</div>,
  (prevProps, nextProps) => {
    // 自定义比较逻辑
    // 如果返回true,表示新旧props相等,不渲染 与shouldComponentUpdate相反
    // 如果返回false,表示新旧props不等,重新渲染
  }
)

useCallback

React -> useCallback

useCallback 是 React 提供的 hook,它用于在多次渲染中 缓存函数

我们重新调整下 App 的代码,将 increment 作为 Childprops,将按钮置于 Child 中:

import React, { useState } from 'react'

const Child = React.memo(({ count, increment }: { count: number, increment: () => void }) => {
  console.log('Child rendered')
  return (
    <div>
      <h1>
        Child Count:
        {count}
      </h1>
      <button onClick={increment}>Increment Count</button>
    </div>
  )
})

function App() {
  const [count, setCount] = useState(0)
  const [otherValue, setOtherValue] = useState('Hello')

  const increment = () => {
    setCount(prevCount => prevCount + 1)
  }

  const changeOtherValue = () => {
    setOtherValue(prevValue => prevValue === 'Hello' ? 'World' : 'Hello')
  }

  console.log('Parent rendered')
  return (
    <div>
      <h1>
        otherValue:
        {otherValue}
      </h1>
      <button onClick={changeOtherValue}>Change Other Value</button>
      <Child count={count} increment={increment} />
    </div>
  )
}

export default App

分别点击 Increment CountChange Other Value 按钮,可以看到:

useCallback-1

otherValue 变化时,AppChild 都重新渲染了, Child 组件并没有使用 otherValue

实际问题是由于 App 每次重新渲染时,都创建了新的 increment 函数,这对于 Child 来说, props 发生了变化,所以重新渲染。

这显然是不必要的,可以使用 useCallback 来缓存 increment 函数:

function App() {
  // ...
  const increment = useCallback(() => {
    setCount(prevCount => prevCount + 1)
  }, [])
  // ...
}

useCallback 接收两个参数,第一个参数是要缓存的函数,第二个参数是一个依赖项数组,如果依赖项发生变化,缓存的函数会重新创建。

在本示例中,由于 increment 比较简单,它不需要依赖项,所以只需要传入一个空数组即可。

分别点击 Increment CountChange Other Value 按钮,可以看到:

useCallback-2

可以看到,当 otherValue 变化时,Child 没有重新渲染了,这符合我们的预期。

实际开发中, React.memouseCallback 通常会一起使用,以减少不必要的组件渲染,从而提高性能。

注意

在某些文章中会提到 useCallback 可以避免不必要的 函数创建 。这种说法是 错误的 !!!

函数组件在每次执行时,都会创建新的执行上下文,在函数作用域内,传入 useCallback 的函数,每次都是一个新的函数。

useCallback 的核心并不是在阻止函数的创建,而是 控制函数引用的更新时机 。从而避免与其相关的组件 由于引用发生变化,导致不必要的重新渲染,从而提高性能。

useMemo

React -> useMemo

useMemo 用于缓存计算结果,避免在每次渲染时重新计算。

它适合在一些计算代价比较大的操作中使用。

说明

useMemo 有些类似于 Vuecomputed 计算属性。

它缓存了当前计算结果,在下次渲染时,只要依赖项没有发生变化,则跳过计算,直接返回缓存的结果。

举个例子,当我们在实现一个购物车功能时,总价需要根据当前购物车中的商品进行计算, 如果每次重新渲染时都需要重新计算总价,可能产生的代价是高昂的,浪费了性能。

我们可以使用 useMemo 来缓存计算结果,避免在每次渲染时重新计算:

function ShoppingCart({ items }) {
  const total = useMemo(() => {
    console.log('计算总价...')
    return items.reduce((sum, item) =>
      sum + item.price * item.quantity, 0)
  }, [items]) // 仅当items变化时重新计算

  return (
    <div>
      总价:
      {total}
    </div>
  )
}

我们回到最开始的 App 应用,增加一个 doubleCount ,它需要根据 count 进行计算:

function App() {
  // ...
  // 处于演示目的,使用一个简单的函数,实际可以直接写为 doubleCount = count * 2
  const doubleCount = () => {
    console.log('doubleCount called')
    return count * 2
  }
  // ...
  console.log('App rendered')
  return (
    <div>
      {/* ... */}
      <p>
        Double Count:
        {doubleCount()}
      </p>
    </div>
  )
}

useMemo-1

可以看到,不管是更新 count 还是更新 otherValuedoubleCount 都会重新计算。 但 doubleCount 仅需要在 count 发生变化时才会重新计算,而 otherValue 变化时没必要重新计算。

此时我们可以使用 useMemo 来缓存计算结果,避免在每次渲染时重新计算:

function App() {
  // ...
  const doubleCount = useMemo(() => {
    console.log('doubleCount called')
    return count * 2
  }, [count])
  // ...
  console.log('App rendered')
  return (
    <div>
      {/* ... */}
      <p>
        Double Count:
        {doubleCount}
      </p>
    </div>
  )
}

useMemo-2

可以看到, 当 count 变化时,doubleCount 重新计算,但 otherValue 变化时,doubleCount 没有重新计算,这符合我们的预期。

对比

特性React.memouseCallbackuseMemo
主要功能缓存组件缓存函数缓存计算结果
使用场景当传入的 props 没有变化时,避免组件重新渲染当函数没有变化时,避免函数重新执行当计算结果没有变化时,避免重新计算
依赖项props依赖数组依赖数组
缓存目标组件实例函数引用计算结果
返回值类型组件函数任意值

总结

React 渲染优化核心在于 精确控制组件更新时机

特别需要注意的是:

  • 所有优化都基于浅比较原理
  • 函数组件优化需要 hooks 的配合使用
  • 复杂数据结构需保证引用更新可预测性

性能优化第一原则

只有在性能监测工具(如 React DevTools Profiler)确认存在性能问题后才进行优化,避免过早优化带来的代码复杂度。