render函数优化与SCU性能优化

44 阅读7分钟

0.render函数优化的意义

在 React 中,render 函数是用于生成虚拟 DOM(Virtual DOM)的核心部分。通过优化render 函数能够减少不必要的计算和 DOM 操作,从而提高应用的性能。

  • 性能提升: 优化 render 函数可以减少渲染所需的计算时间,降低 CPU 使用率,提高应用的性能。
  • 减少不必要的渲染: React 使用虚拟 DOM 进行比对,找出哪些部分需要更新,然后只更新这些部分。通过优化 render 函数,可以减少虚拟 DOM 的比对次数,从而减少不必要的渲染。
  • 更好的用户体验: 快速渲染和响应的应用程序可以提供更好的用户体验,用户不会感到卡顿或延迟。

1.shouldComponentUpdate

shouldComponentUpdate 是 React 组件生命周期中的一个方法,用于决定组件是否需要进行重新渲染。它在组件即将重新渲染之前被调用,允许开发者根据组件的属性(props)和状态(state)的变化来判断是否需要更新组件的输出。

shouldComponentUpdate 方法接收两个参数:nextProps 和 nextState,它们表示组件即将接收的新属性和新状态。在方法内部,你可以编写逻辑来判断是否需要重新渲染组件。如果返回 true,则组件将继续执行更新过程;如果返回 false,则组件将不会进行更新,其 render 方法也不会被调用。

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 在这里判断是否需要重新渲染组件
    // 返回 true 表示需要重新渲染,返回 false 表示不需要重新渲染
  }

  render() {
    // 组件的渲染逻辑
  }
}

2.案例

2.1 比较state状态是否相同

通过一个简单的案例来说明render函数渲染优化。案例中App组件展示message和count两个数据,并且引用了子组件Home和Banner,这两个组件分别接受父组件传入的message和count做展示,代码运行过程中,只要App进行数据修改,所有组件都会重新执行render,进行diff算法。然而一些数据没有发生变更的子组件没必要重新执行render函数。

import React, { Component } from "react";
import Home from "./Home";
import Banner from "./Banner";
class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      message: "Hello World",
      count: 0,
    };
  }
  shouldComponentUpdate(nextProps, newState) {
    // 在这里判断是否需要重新渲染组件
    if (this.state.message !== newState.message) {
      return true;
    }
    return false;
  }
  changeText() {
    // this.setState({ message: "Hello USTB" });
    this.setState({ message: "Hello World" });
  }
  render() {
    console.log("App 执行render");
    const { message, count } = this.state;

    return (
      <div>
        <h2>
          App:{message}-{count}
        </h2>
        <button
          onClick={(e) => {
            this.changeText();
          }}
          >
          修改文本
        </button>
        <Home message={message} />
        <Banner count={count} />
      </div>
    );
  }
}

export default App;

重新刷新页面时,三个组件都会重新执行render函数。

如下图所示,如果此时修改message值仍然为HelloWorld时,并且将SCU注释掉,更改message仍然会触发三个组件的render重新执行。

然而此时页面并没有发生变化,这时重新执行render函数是增加性能开销的,所以在SCU方法中,在新状态 newState 中比较了 message 的值是否与旧状态的值不同。如果不同,返回 true,表示需要进行重新渲染;如果相同,返回 false,表示不需要重新渲染。因此加上SCU函数后,如果message值前后没发生变化,就不执行render函数。

2.2比较props是否相同

以子组件Banner为例,Banner接受父组件App传入的参数count,如果父组件点金更改文字按钮,即使没有对count的值进行更新,如果执行了render函数,那么默认Banner子组件也会执行render函数,但是此时Banner中的count并没有发生改变,因此可以为Banner组件编写shouldComponentUpdate方法。在这个方法中,通过比较传入的属性 nextProps 中的 count 是否与当前的 props.count 相等,来判断是否需要进行重新渲染。如果不相等,返回 true,表示需要重新渲染;如果相等,返回 false,表示不需要重新渲染。

实现了Banner组件只在属性 count 发生变化时才会触发重新渲染。


import React, { Component } from "react";

class Banner extends Component {
  constructor(props) {
    super(props);
    this.state = {};
  }
  shouldComponentUpdate(nextProps, newState) {
    if (nextProps.count !== this.props.count) {
      return true;
    }
    return false;
  }
  render() {
    console.log("Banner 执行render");
    const { count } = this.props;
    return (
      <div>
        <h2>Banner: {count}</h2>
      </div>
    );
  }
}
export default Banner;

点击按钮,可以发现只有App和Home组件进行了render,因为此时Banner中的count并没有发生变化,因此不需要执行render函数。

3.PureComponent

PureComponent 是 React 中的一个特殊类型的组件,它是 Component 类的一个子类,用于优化组件的性能。PureComponent 的主要特点是,它在 shouldComponentUpdate 方法中实现了一种浅比较,用于自动判断是否需要重新渲染。这意味着只有在组件的状态或者传入的属性发生浅层次的变化时,PureComponent 才会触发重新渲染,否则它会阻止不必要的渲染。

import React, { PureComponent } from "react";

class MyComponent extends PureComponent {
  render() {
    const { value } = this.props;
    return <div>{value}</div>;
  }
}

在上面的示例中,MyComponent 继承了 PureComponent,所以它会自动在 shouldComponentUpdate 中进行浅比较,只有在传入的 value 属性发生变化时才会重新渲染。这有助于优化性能,特别是在大型应用中。


  • 自动浅比较: PureComponent 在 shouldComponentUpdate 方法中会自动对当前状态和属性与下一次更新的状态和属性进行浅比较。如果它们相等,PureComponent 不会触发重新渲染,从而避免不必要的性能开销。
  • 适用场景: PureComponent 特别适用于那些只依赖于它们的属性和状态的组件。它们不会受到父组件的影响,也不会对其他状态或上下文造成影响。
  • 注意事项: 尽管 PureComponent 能够自动进行浅比较,但如果相关属性或状态是引用类型(如数组、对象),并且发生了改变,但其引用仍然保持不变,那么 PureComponent 可能无法检测到变化。这是因为它只进行浅比较,而不会递归比较引用类型的内容。

因此可以将Banner组件变为PureComponent组件,这样就不用再手动实现shouldComponentUpdate,并且修改父组件的message时,Banner组件不会执行render函数。

import React, { PureComponent } from "react";

class Banner extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {};
  }

  render() {
    console.log("Banner 执行render");
    const { count } = this.props;
    return (
      <div>
        <h2>Banner: {count}</h2>
      </div>
    );
  }
}

export default Banner;

4.memo

React.memo 是一个函数组件的包装器,用于优化函数组件的性能。它类似于 PureComponent,但适用于函数组件,帮助避免不必要的重新渲染。memo 最终返回一个新的 React 组件。它的行为与提供给 memo 的组件相同,只是当它的父组件重新渲染时 React 不会总是重新渲染它,除非它的 props 发生了变化。

import { memo } from 'react';

const SomeComponent = memo(function SomeComponent(props) {
  // ...
});

借用官方文档的一段话,只要该组件的 props 没有改变,这个记忆化版本就不会在其父组件重新渲染时重新渲染。但 React 仍可能会重新渲染它:记忆化是一种性能优化,而非保证

  • Component:要进行记忆化的组件。memo 不会修改该组件,而是返回一个新的、记忆化的组件。它接受任何有效的 React 组件,包括函数组件和 forwardRef 组件。
  • 可选参数 arePropsEqual:一个函数,接受两个参数:组件的前一个 props 和新的 props。如果旧的和新的 props 相等,即组件使用新的 props 渲染的输出和表现与旧的 props 完全相同,则它应该返回 true。否则返回 false。通常情况下,你不需要指定此函数。默认情况下,React 将使用 Object.is 比较每个 prop

下列代码使用了 memo 函数来优化函数组件 Profile。这个组件在传入的 props 中接收了一个名为 count 的属性。在使用 memo 包装的情况下,Profile 组件将只在传入的 count 属性发生变化时进行重新渲染。如果其他属性不发生变化,组件将避免重新渲染,从而提升性能。

当在父组件中更新 count 属性时,只有在 count 属性的值发生变化时,Profile 组件会重新渲染。如果其他属性发生变化或者父组件没有重新渲染,那么 Profile 组件的重新渲染操作将被跳过,这有助于提升应用的性能。

import { memo } from "react";

const Profile = memo(function (props) {
  console.log("Profile 执行render");
  return <h2>Profile: {props.count}</h2>;
});
export default Profile;