React 性能优化

React 性能优化

React 应用也是前端应用,前端项目普适的性能优化手段,比如资源加载过程中的优化、减少重绘与回流、服务端渲染、启用 CDN 等,这些手段对于 React 来说也是同样奏效的。

但是 React 区别于传统的前端项目特点就是 以组件的形式来组织代码,React 允许我们把一个大的 UI 界面,拆分出可复用的代码片段(也就是组件),可以对每一部分的代码片段进行独立构思。因此,React 组件有一些特色的性能优化思路。接下来,我们开始讲解 React 性能优化的思路!

React 组件性能优化的手段,主要有三种:

  • 使用 shouldComponentUpdate 来阻止不必要的子组件的 ‘re-render’(重渲染)
  • PureComponent + immutable.js
  • React.memo 与 useMemo

1. 基于 shouldComponentUpdate 优化

render 方法由于伴随着对虚拟 DOM 的构建和对比,这其中的过程非常耗时,而在 React 中,很多时候会不经意的调用 ‘re-render’,为了避免不必要的 render 带来的性能消耗,因此出现了 shuouldComponentUpdate 这个生命周期。

shouldComponentUpdate 是 React 更新阶段的一个生命周期,React 组件会根据 shouldComponentUpdate 这个生命周期的返回值,来决定是否执行该方法之后的生命周期,进而决定是否对组件进行 ‘re-render’(重渲染)

接下来直接代码展示

分别创建 home, childA , childB 三个组件(home是主页面,childA, childB分别是它的子组件)

home.jsx

import React, { Component } from 'react'

import ChildA from "./childA"
import ChildB from "./childB"

export default class Home extends Component {
  state = {
    childA: '文本A',
    childB: '文本B',
  }

  changeA = () => {
    this.setState({
      childA: '改变文本A'
    })
  }

  changeB = () => {
    this.setState({
      childB: '改变文本B'
    })
  }

  render() {

    const {childA, childB} = this.state

    return (
      <div>
        <button onClick={this.changeA}>改变childA的文本</button>
        <button onClick={this.changeB}>改变childB的文本</button>
        <ChildA textA={childA}></ChildA>
        <ChildB textB={childB}></ChildB>
      </div>
    )
  }
}

childA.jsx

import React, { Component } from 'react'

export default class ChildA extends Component {
  render() {

    console.log('childA 被 render')

    return (
      <div>
        <h3>A组件文本:{this.props.textA}</h3>
      </div>
    )
  }
}

childB.jsx

import React, { Component } from 'react'

export default class ChildB extends Component {
  render() {

    console.log('childB 被 render')

    return (
      <div>
        <h3>B组件文本:{this.props.textB}</h3>
      </div>
    )
  }
}

效果如下:

组件初始化时:打印台输出,childA. childB 都被render

在这里插入图片描述

点击 改变childA文本 按钮时:打印台输出, childA, childB 也都被改变

在这里插入图片描述

上图所示,组件初始化 不加 shouldComponentUpdate 情况下,当给 childB.jsx 加上shouldComponentUpdate之后

import React, { Component } from 'react'

export default class ChildB extends Component {

  shouldComponentUpdate(nextProps, nextState) {
    // 判断 text 属性在父组件更新前后有没有发生变化,若没有发生变化,则返回 false
    if(nextProps.textB === this.props.textB) {
      return false
    }
    // 只有在 text 属性值确实发生变化时,才允许更新进行下去
    return true
  }

  render() {

    console.log('childB 被 render')

    return (
      <div>
        <h3>B组件文本:{this.props.textB}</h3>
      </div>
    )
  }
}

效果:

打印台输出, 只有 childA 被render

在这里插入图片描述

因此我们得出结论,在 React 中,只要父组件发生了更新,那么所有的子组件都会被无条件的更新,这就导致了无论 props 是否改变,子组件都会被重新 render。

2. 基于 PureComponent + immutable.js 优化

pureComponent 是在 shouldComponentUpdate 衍生出来的玩法。它的功能也是避免 re-render。

PureComponent 与 Component 的区别点,就在于它内置了对 shouldComponentUpdate 的实现:PureComponent 将会在 shouldComponentUpdate 中对组件更新前后的 props 和 state 进行浅比较,并根据浅比较的结果,决定是否需要继续更新流程。

“浅比较”将针对值类型数据对比其值是否相等,而针对数组、对象等引用类型的数据则对比其引用是否相等。

这次把 childB.jsx 修改成 PureComponent, 代码如下:

import React from "react";
export default class ChildB extends React.PureComponent {
  render() {
    console.log("ChildB 的render方法执行了");
    return (
      <div className="childB">
        子组件B的内容:
        {this.props.text}
      </div>
    );
  }
}

实现了和 shouldComponentUpdate 一样的效果。

但是,PureComponent 如果数据类型为引用类型,那么这种基于浅比较的判断逻辑就会带来这样两个风险:

  1. 若数据内容没变,但是引用变了,那么浅比较仍然会认为“数据发生了变化”,进而触发一次不必要的更新,导致过度渲染;
  2. 若数据内容变了,但是引用没变,那么浅比较则会认为“数据没有发生变化”,进而阻断一次更新,导致不渲染。
解决方法 Immutable

Immutable 它的意思就是 “持久性数据”,指的是这个数据只要被创建出来了,就不能被更改。我们对当前数据的任何修改动作,都会导致一个新的对象的返回。这就将数据内容的变化和数据的引用严格地关联了起来,侦听到所有的变化。

举个栗子:

// 引入 immutable 库里的 Map 对象,它用于创建对象
import { Map } from 'immutable'
// 初始化一个对象 baseMap
const baseMap = Map({
  name: '小明',
  age: 99
})
// 使用 immutable 暴露的 Api set 修改 baseMap 的内容
const changedMap = baseMap.set({
  age: 100
})
// 我们会发现修改 baseMap 后将会返回一个新的对象,这个对象的引用和 baseMap 是不同的
console.log(baseMap === changedMap)  // false

3. 基于 React.memo 和 useMemo 函数组件性能优化

React.memo

相当于函数版 shouldComponentUpdate / Purecomponent,它是 React 导出的一个顶层函数,它的本质是一个高阶组件,负责对函数组件进行包装。

使用方法如下:

import React from "react";
// 定义一个函数组件
function Home(props) {
  return xxx
}
// compare 函数是 memo 的第二个入参,我们之前放在 shouldComponentUpdate 里面的逻辑就可以转移至此处
function compare(prevProps, nextProps) {
  if(preProps === nextProps) {
      return false
  }
  return true
}
// 使用 React.memo 来包装函数组件
export default React.memo(Home, compare);

从示例中我们可以看出,React.memo 接收两个参数,第一个参数是我们需要渲染的目标组件第二个参数 compare则用来承接 props 的对比逻辑之前我们在 shouldComponentUpdate 里面做的事情,现在就可以放在 compare里来做

把 childA 优化:

import React from "react"

// 将 ChildA 改写为 function 组件
function ChildB(props) {

  console.log("ChildA 被 render");

  return (
    <div>
      <h3>A组件文本:{props.textA}</h3>
    </div>
  )
}

// compare 用于对比 props 的变化,只有当相同的时候返回 true 开启 React.memo
function compare(prevProps, nextProps) {
  if(nextProps.textA === prevProps.textA) {
    return true
  }
  return false
}

// 使用 React.memo 来包装 ChildB

export default React.memo(ChildB, compare);

这里的 compare 函数是一个可选参数,当我们不传入 compare 时,React.memo 也可以工作,此时它的作用就类似于 PureComponent——React.memo 会自动为你的组件执行 props 的浅比较逻辑

和 shouldComponentUpdate 不同的是,React.memo 只负责对比 props,而不会去感知组件内部状态(state)的变化。

useMemo

useMemo 是一种更加精细的 Memo,React.memo 可以实现类似于 shouldComponentUpdate 或者 PureComponent 的效果,对组件级别的 re-render 进行管控。但是有时候,我们希望复用的并不是整个组件,而是组件中的某一个或几个部分,就需要 useMemo 来帮忙了。

因此,React.memo 控制是否需要重渲染一个组件,而 useMemo 控制的则是是否需要重复执行某一段逻辑。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

我们可以把目标逻辑作为第一个参数传入,把逻辑的依赖项数组作为第二个参数传入。这样只有当依赖项数组中的某个依赖发生变化时,useMemo 才会重新执行第一个入参中的目标逻辑。

新加一个 childC.jsx:(使用useMemo)

import React, { useMemo } from 'react'

function ChildC({text, num}) {

  console.log('childC 被 render');

  const renderText = (text) => {
    console.log('childC renderText执行了');
    return <span>{text}</span>
  }

  const renderNum = (num) => {
    console.log('childC renderNum执行了');
    return <span>{num}</span>
  }

  const textMemo = useMemo(() => renderText(text), [text])
  const numMemo = useMemo(() => renderNum(num), [num])

  return (
    <div>
      <h3>child3内容是:{textMemo}</h3>
      <h3>child3数字是:{numMemo}</h3>
    </div>
  )
}

export default ChildC

同时修改 home.jsx

import React, { Component } from 'react'

import ChildA from "./childA"
import ChildB from "./childB"
import ChildC from "./childC"

export default class Home extends Component {
  state = {
    childA: '文本A',
    childB: '文本B',
    childC: {
      text: '文本C',
      num: 10
    }
  }

  changeA = () => {
    this.setState({
      childA: '改变文本A'
    })
  }

  changeB = () => {
    this.setState({
      childB: '改变文本B'
    })
  }

  changeC = () => {
    this.setState({
      childC: {
        ...this.state.childC,
        num: 100
      }
    })
  }

  render() {

    const {childA, childB, childC} = this.state

    return (
      <div>
        <button onClick={this.changeA}>改变childA的文本</button>
        <button onClick={this.changeB}>改变childB的文本</button>
        <button onClick={this.changeC}>改变childC的文本</button>
        <ChildA textA={childA}></ChildA>
        <ChildB textB={childB}></ChildB>
        <ChildC {...childC}></ChildC>
      </div>
    )
  }
}

效果如下图所示:

在这里插入图片描述

效果如图所示,检测到了只改变了childC中的num,所以组件 ChildC 仅仅 render 了 num , 没有重新render text。

使用 useMemo,我们可以对函数组件的执行进行更加精细的管控(尤其是定向规避掉一些高开销的计算),同时也弥补了 React.memo 无法感知函数内部状态变化的缺点。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值