react 性能优化

React 组件性能优化的核心就是减少渲染真实DOM节点的频率,减少Virtual DOM 对比的频率,以此来提高性能

1. 组件卸载之前进行清理操作

在组件中为window 注册的全局事件,以及定时器,在组件卸载前要清理掉,防止组件卸载后继续执行影响应用性能

我们开启一个定时器然后卸载组件,查看组件中的定时器是否还在运行 Test 组件来开启一个定时器

import {useEffect} from 'react'

export default function Test () {
  useEffect(() => {
    setInterval(() => {
      console.log('定时器开始执行')
    }, 1000)
  }, [])
  return <div>Test</div>
}

在App.js中引入定时器组件然后用flag变量来控制渲染和卸载组件

import Test from "./Test";
import { useState } from "react"
function App() {
  const [flag, setFlag] = useState(true)
  return (
    <div>
      { flag && <Test /> }
      <button onClick={() => setFlag(prev => !prev)}>点击按钮</button>
    </div>
  );
}

export default App;

在浏览器中我们去点击按钮发现组件被卸载后定时器还在执行,这样组件太多之后或者这个组件不停的渲染和卸载会开启很多的定时器,我们应用的性能肯定会被拉垮,所以我们需要在组建卸载的时候去销毁定时器。

import {useEffect} from 'react'

export default function Test () {
  useEffect(() => {
    // 因为要销毁定时器所以我们需要用一个变量来接受定时器id
    const InterValTemp =  setInterval(() => {
      console.log('定时器开始执行')
    }, 1000)
    return () => {
      console.log(`ID为${InterValTemp}定时器被销毁了`)
      clearInterval(InterValTemp)
    }
  }, [])
  return <div>Test</div>
}

这个时候我们在去点击销毁组建的时候定时器就被销毁掉了

2. 类组件用纯组件来提升组建性能PureComponent

1. 什么是纯组件

​ 纯组件会对组建的输入数据进行浅层比较,如果输入数据和上次输入数据相同,组建不会被重新渲染

2. 什么是浅层比较

​ 比较引用数据类型在内存中的引用地址是否相同,比较基本数据类型的值是否相同

3. 如何实现纯组件

​ 类组件集成 PureComponent 类,函数组件使用memo方法

4. 为什么不直接进行diff操作,而是要进行浅层比较,浅层比较难到没有性能消耗吗

​ 和进行 diff 比较操作相比,浅层比较小号更少的性能,diff 操作会重新遍历整个 virtualDOM 树,而浅层比较只比较操作当前组件的 state和props

在状态中存储一个name为张三的,在组建挂载后我们每隔1秒更改name的值为张三,然后我们看纯组件和非纯组件,查看结果

// 纯组件
import { PureComponent } from 'react'
class PureComponentDemo extends PureComponent {
  render () {
    console.log("纯组件")
    return <div>{this.props.name}</div>
  }
}
// 非纯组件
import { Component } from 'react'
class ReguarComponent extends Component {
 render () {
   console.log("非纯组件")
   return <div>{this.props.name}</div>
 }
}

引入纯组件和非纯组件 并在组件挂在后开启定时器每隔1秒更改name的值为张三

import { Component } from 'react'
import { ReguarComponent, PureComponentDemo } from './PureComponent'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
  }
  updateName () {
    setInterval(() => {
      this.setState({name: "张三"})
    }, 1000)
  }
  componentDidMount () {
    this.updateName()
  }
  render () {
    return <div>
      <ReguarComponent name={this.state.name}></ReguarComponent>
      <PureComponentDemo name={this.state.name}></PureComponentDemo>
    </div>
  }
}

打开浏览器查看执行结果

image-20210922214700974

我们发现纯组件只执行了一次,以后在改相同的值的时候,并没有再重新渲染组件,而非纯组件则是每次更改都在重新渲染,所以纯组件要比非纯组件更节约性能

3. 函数组件来实现纯组件 memo

  1. memo 基本使用

将函数组件变成纯组件,将当前的props和上一次的props进行浅层比较,如果相同就组件组件的渲染。》。

我们在父组件中维护两个状态,index和name 开启定时器让index不断地发生变化,name传递给子组件,查看父组件更新子组件是否也更新了, 我们先不用memo来查看结果

import { useState, useEffect } from 'react'
function App () {
  const [ name ] = useState("张三")
  const [index, setIndex] = useState(0)

  useEffect(() => {
    setInterval (() => {
      setIndex(prev => prev + 1)
    }, 1000)
  }, [])

  return <div>
    {index}
    <ShowName name={name}></ShowName>
  </div>
}

function ShowName ({name}) {
  console.log("组件被更新")
  return <div>{name}</div>
}

打开浏览器查看执行结果

image-20210923231543043

在不使用 memo 来把函数组件变成纯组件的情况下我们发现子组件随着父组件更新而一起重新渲染,但是它依赖的值并没有更新,这样浪费了性能,我们使用 memo 来避免没必要的更新

import { useState, useEffect, memo } from 'react'

const ShowName = memo(function ShowName ({name}) {
  console.log("组件被更新")
  return <div>{name}</div>
})

function App () {
  const [ name ] = useState("张三")
  const [index, setIndex] = useState(0)

  useEffect(() => {
    setInterval (() => {
      setIndex(prev => prev + 1)
    }, 1000)
  }, [])

  return <div>
    {index}
    <ShowName name={name}></ShowName>
  </div>
}

我们再次打开浏览器查看执行结果

image-20210922222640420

现在index变动 子组件没有重新渲染了,用 memo 把组件变为纯组件之后就避免了依赖的值没有更新却跟着父组件一起更新的情况

4. 函数组件来实现纯组件(为memo方法传递自定义比较逻辑)

memo 方法也是浅层比较

memo 方法是有第二个参数的第二个参数是一个函数

这个函数有个两个参数,第一个参数是上一次的props,第二个参数是下一个props

这个函数返回 false 代表重新渲染, 返回true 重新渲染

比如我们有员工姓名和职位两个数据,但是页面中只使用了员工姓名,那我们只需要观察员工姓名发生变动没有,所以我们在memo的第二个参数去比较是否需要重新渲染

import { useState, useEffect, memo } from 'react'

function compare (prevProps, nextProps) {
  if (prevProps.person.name !== nextProps.person.name) {
    return false
  }
  return true
}

const ShowName = memo(function ShowName ({person}) {
  console.log("组件被更新")
  return <div>{person.name}</div>
}, compare)

function App () {
  const [ person, setPerson ] = useState({ name: "张三", job: "工程师"})

  useEffect(() => {
    setInterval (() => {
      setPerson({
        ...person,
        job: "挑粪"
      })
    }, 1000)
  }, [person])

  return <div>
    <ShowName person={person}></ShowName>
  </div>
}

5. shouldComponentUpdata

纯组件只能进行浅层比较,要进行深层次比较,使用 shouldComponentUpdate,它用于编写自定义比较逻辑

返回true 重新渲染组件, 返回 false 组件重新渲染组件

函数的第一个参数为 nextProps,第二个参数为NextState

比如我们有员工姓名和职位两个数据,但是页面中只使用了员工姓名,那我们只需要观察员工姓名发生变动没有,利用shouldComponentUpdata来控制只有员工姓名发生变动才重新渲染组件,我们查看使用 shouldComponentUpdata 生命周期函数和不使用shouldComponentUpdata生命周期函数的区别

// 没有使用的组件
import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      person: {
        name: '张三',
        job: '工程师'
      }
    }
  }
  componentDidMount (){
    setTimeout (() => {
      this.setState({
        person: {
          ...this.state.person,
          job: "修水管"
        }
      })
    }, 2000) 
  }
  render () {
    console.log("render 方法执行了")
    return <div>
      {this.state.person.name}
    </div>
  }
}

我们打开浏览器等待两秒

image-20210922220251277

发现render方法执行了两次,组件被重新渲染了,但是我们并没有更改name 属性,所以这样浪费了性能,我们用shouldComponentUpdata生命周期函数来判断name是否发生了改变

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      person: {
        name: '张三',
        job: '工程师'
      }
    }
  }
  componentDidMount (){
    setTimeout (() => {
      this.setState({
        person: {
          ...this.state.person,
          job: "修水管"
        }
      })
    }, 2000) 
  }
  render () {
    console.log("render 方法执行了")
    return <div>
      {this.state.person.name}
    </div>
  }
  shouldComponentUpdate (nextProps, nextState) {
    if (this.state.person.name !== nextState.person.name) {
      return true;
    }
    return false;
  }
}

我们再打开浏览器等待两秒之后

image-20210922220711461

我们只改变了job 的时候render方法只执行了一次,这样就减少了没有必要的渲染,从而节约了性能

6. 使用组件懒加载

使用路由懒加载可以减少bundle文件大小,从而加快组建呈递速度

创建 Home 组建

// Home.js
function Home() {
  return (
    <div>
      首页
    </div>
  )
}

export default Home

创建 List 组建

// List.js
function List() {
  return (
    <div>
      列表页
    </div>
  )
}

export default List

从react-router-dom包中引入 BrowserRouter, Route, Switch, Link 和 home 与list 来创建路由规则以及切换区域和跳转按钮

import { BrowserRouter, Route, Switch, Link } from 'react-router-dom'
import Home from './Home';
import List from './List';

function App () {
  return <div>
    <BrowserRouter>
        <Link to="/">首页</Link>
        <Link to="/list">列表页</Link>
      <Switch>
          <Route path="/" exact component={Home}></Route>
          <Route path="/list" component={List}></Route>
      </Switch>
    </BrowserRouter>
  </div>
}

使用 lazy, Suspense 来创建加载区域与加载函数

import { lazy, Suspense } from 'react';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom'

const Home = lazy(() => import('./Home'))
const List = lazy(() => import('./List'))

function Loading () {
  return <div>loading</div>
}

function App () {
  return <div>
    <BrowserRouter>
        <Link to="/">首页</Link>
        <Link to="/list">列表页</Link>
      <Switch>
        <Suspense fallback={<Loading />}>
          <Route path="/" exact component={Home}></Route>
          <Route path="/list" component={List}></Route>
        </Suspense>
      </Switch>
    </BrowserRouter>
  </div>
}

使用注解方式来为打包后的文件命名

const Home = lazy(() => import(/* webpackChunkName: "Home"  */'./Home'))
const List = lazy(() => import(/* webpackChunkName: "List" */'./List'))

7. 根据条件进行组件懒加载

适用于组件不会随条件频繁切换

import { lazy, Suspense } from 'react';


function App () {
  let LazyComponent = null;
  if (false){
    LazyComponent = lazy(() => import(/* webpackChunkName: "Home"  */'./Home'))
  } else {
    LazyComponent = lazy(() => import(/* webpackChunkName: "List" */'./List'))
  }
  return <div>
    <Suspense fallback={<div>loading</div>}>
      <LazyComponent />
    </Suspense>
  </div>
}

export default App;

这样就只会加载一个组件从而提升性能

8. 通过使用占位符标记提升React组件的渲染性能

React组件中返回的jsx如果有多个同级元素必须要有一个共同的父级

function App () {
  return (<div>
        <div>1</div>
      <div>2</div>
    </div>)
}

为了满足这个条件我们通常会在外面加一个div,但是这样的话就会多出一个无意义的标记,如果每个元素都多处这样的一个无意义标记的话,浏览器渲染引擎的负担就会加剧

为了解决这个问题,React 推出了 fragment 占位符标记,使用占位符编辑既满足了共同父级的要求,也不会渲染一个无意义的标记

import { Fragment } from 'react'
function App () {
  return <Fragment>
          <div>1</div>
        <div>1</div>
  </Fragment>
}

当然 fragment 标记还是太长了,所以有还有简写方法

function App () {
  return <>
          <div>1</div>
        <div>1</div>
  </>
}

9. 不要使用内联函数定义

在使用内联函数后,render 方法每次运行后都会创建该函数的新实例,导致 React 在进行 Virtual DOM 对比的时候,新旧函数比对不相等,导致 React 总是为元素绑定新的函数实例,而旧的函数有要交给垃圾回收器处

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={() => { this.setState({name: "李四"})}}>修改</button>
    </div>
  }
}


export default App;

修改为以下的方式

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={this.setChangeName}>修改</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "李四"})
  }
}

10. 在构造函数中进行函数this绑定

在类组件中如果使用 fn(){} 这种方式定义函数,函数的 this 指向默认只想 undefined,也就是说函数内部的 this 指向需要被更正,

可以在构造函数中对函数进行 this 更正,也可以在内部进行更正,两者看起来没有太大差别,但是对性能影响是不同的

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
    // 这种方式应为构造器只会执行一次所以只会执行一次
    this.setChangeName = this.setChangeName.bind(this)
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      {/* 这种方式在render方法执行的时候就会生成新的函数实例 */}
      <button onClick={this.setChangeName.bind(this)}>修改</button>
    </div>
  }
  setChangeName() {
    this.setState({name: "李四"})
  }
}

在构造函数中更正this指向只会更正一次,而在render方法中如果不更正this指向的话 那么就是 undefined ,但是在render方法中更正的话render方法的每次执行都会返回新的函数实例这样是对性能是有所影响的

11. 类组件中的箭头函数

在类组件中使用箭头函数不会存在this指向问题,因为箭头函数不绑定this

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      {/* <button onClick={() => { this.setState({name: "李四"})}}>修改</button> */}
      <button onClick={this.setChangeName}>修改</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "李四"})
  }
}

箭头函数在this指向上确实比较有优势

但是箭头函数在类组件中作为成员使用的时候,该函数会被添加成实例对象属性,而不是原型对象属性,如果组件被多次重用,每个组件实例都会有一个相同的函数实例,降低了函数实例的可用性造成了资源浪费

综上所述,我们得出结论,在使用类组件的时候还是推荐在构造函数中通过使用bind方法更正this指向问题

12. 避免使用内联样式属性

当使用内联样式的时候,内联样式会被编译成JavaScript代码,通过javascript代码将样式规则映射到元素身上,浏览器就会画更多的时间执行脚本和渲染UI,从而增加了组件的渲染时间

function App () {
  return <div style={{backgroundColor: 'red';}}></div>
}

在上面的组件中,为元素增加了背景颜色为红色,这个样式为JavaScript对象,背景颜色需要被转换成等效的css规则,然后应用到元素上,这样涉及了脚本的执行,实际上内联样式的问题在于是在执行的时候为元素添加样式,而不是在编译的时候为元素添加样式

更好的方式是导入样式文件,能通过css直接做的事情就不要通过JavaScript来做,因为JavaScript操作 DOM 非常慢

13. 优化条件渲染以提升组件性能

频繁的挂在和卸载组件是一件非常耗性能的事情,应该减少组件的挂载和卸载次数,

在React中 我们经常会通过不同的条件渲染不同的组件,条件渲染是一必须做的优化操作.

function App () {
  if (true) {
    return <div>
      <Component1 />
        <Component2 />
      <Component3 />
      </div>
  } else {
    return <div>
        <Component2 />
          <Component3 />
    </div>
  }
  
}

上面的代码中条件不同的时候,React 内部在进行Virtual DOM 对比的时候发现第一个元素和第二个元素都已经发生变化,所以会卸载组件1、组件2、组件3,然后再渲染组件2、组件3。实际上变化的只有组件1,重新挂在组件2和组件3时没有必要的

function App () {
  if (true) {
    return <div>
      { true && <Component1 />}
        <Component2 />
      <Component3 />
      </div>
  }
}

这样变化的就只有组件1了节省了不必要的渲染

16. 避免重复的无限渲染

当应用程序状态更改的时候,React 会调用 render方法 如果在render方法中继续更改应用程序状态,就会发生递归调用导致应用报错

image-20210923220549762

未捕获错误:超出最大更新深度。当组件在componentWillUpdate或componentDidUpdate内重复调用setState时,可能会发生这种情况。React限制嵌套更新的数量以防止无限循环。React限制的最大次数为50次

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: '张三'
    }
  }
  render () {
    this.setState({name:"张五"})
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={this.setChangeName}>修改</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "李四"})
  }
}

与其他生命周期函数不同,render 方法应该被作为纯函数,这意味着,在render方法中不要做以下事情

  1. 不要调用 setState 方法去更改状态、
  2. 不要使用其他手段查询更改 DOM 元素,以及其他更改应用程序的操作、
  3. 不要在componentWillUpdate生命周期中重复调用setState方法更改状态、
  4. 不要在componentDidUpdate生命周期中重复调用setState方法更改状态、

render方法执行根据状态改变执行,这样可以保持组件的行为与渲染方式一致

15. 为组件创建错误边界

默认情况下,组件渲染错误会导致整个应用程序中断,创建错误边界可以确保组件在发生错误的时候应用程序不会中断,错误边界是一个React组件,可以捕获子级组件在渲染是发生错误,当错误发生时,可以记录下来,可以显示备用UI界面,

错误边界涉及到两个生命周期,分别是 getDerivedStateFromError 和 componentDidCatch.

getDerivedStateFromError 为静态方法,方法中需要返回一个对象,该对象会和state对象进行合并,用于更改应用程序状态.

componentDidCatch 方法用于记录应用程序错误信息,该方法返回的是错误对象

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      hasError: false
    }
  }
  componentDidCatch (error) {
    console.log(error)
  }
  static getDerivedStateFromError () {
    return {
      hasError: true
    }
  }
  render () {
    if (this.state.hanError) {
      return <div>
        发生错误了
      </div>
    }
    return <Test></Test>
  }
}

class Test extends Component {
  constructor () {
    super()
    this.state = {
      hanError: false
    }
  }
  render () {
    throw new Error("发生了错误");
    return <div>
      正确的
    </div>
  }
}

当我们抛出错误的时候,getDerivedStateFromError 会合并返回的对象到state 所以hasError会变成true 就会渲染我们备用的界面了

注意: getDerivedStateFromError 不能捕获异步错误,譬如按钮点击事件发生后的错误

16. 避免数据结构突变

组件中 props 和 state 的数据结构应该保持一致,数据结构突变会导致输出不一致

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      man: {
        name: "张三",
        age: 18
      }
    }
    this.setMan = this.setMan.bind(this)
  }
  render () {
    const { name, age } = this.state.man
    return <div>
      <p>
        {name}
        {age}
      </p>
      <button onClick={this.setMan}>修改</button>
    </div>
  }
  setMan () {
    this.setState({
      ...this.state,
      man: {
        name: "李四"
      }
    })
  }
}

乍一看这个代码貌似没有问题,仔细一看我们发现,在我们修改了名字之后年龄字段丢失了,因为数据突变了 ,我们应该去避免这样的数据突变

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      man: {
        name: "张三",
        age: 18
      }
    }
    this.setMan = this.setMan.bind(this)
  }
  render () {
    const { name, age } = this.state.man
    return <div>
      <p>
        {name}
        {age}
      </p>
      <button onClick={this.setMan}>修改</button>
    </div>
  }
  setMan () {
    this.setState({
      man: {
        ...this.state.man,
        name: "李四"
      }
    })
  }
}

17. 依赖优化

在应用程序中我们经常使用地三方的包,但我们不想引用包中的所有代码,我们只想用到那些代码就包含那些代码,此时我们可以使用插件对依赖项进行优化

我们使用 lodash 举例子. 应用基于 create-react-app 脚手架创建

1. 下载依赖

npm install react-app-rewired customize-cra lodash babel-plugin-lodash

react-app-rewired: 覆盖create-react-app 配置

module.exports = function (oldConfig) {
      return    newConfig
}

customize-cra: 导出辅助方法,可以让以上写法更简洁

const { override, useBabelRc } = require("customize-cra")
module.exports = override(
    (oldConfig) => newConfig,    
  (oldConfig) => newConfig,
)

override: 可以接收多个参数,每个参数都是一个配置函数,函数接受oldConfig,返回newConfig

useBabelRc:允许使用.babelrc 文件进行babel 配置

babel-plugin-lodash:对lodash 进行精简

2. 在项目的根目录新建 config-overrides.js 并加入以下配置

const { override, useBabelRc } = require("customize-cra")

module.exports = override(useBabelRc())

3. 修改package.json文件中的构建命令

{
  "script": {
       "start": "react-app-rewired start",
       "build": "react-app-rewired build",
       "test": "react-app-rewired test --env=jsdom",
       "eject": "react-scripts eject"
  }
}

4. 创建 .babelrc 文件并加入配置

{
  "plugins": ["lodash"]
}

5. 生产环境下的三种 JS文件

  1. main.[hash].chunk.js:这是你的应用程序代码,App.js 等.
  2. 1.[hash].chunk.js:这是第三方库的代码,包含你在 node_modules 中导入的模块.
  3. runtime~main.[hash].js:webpack 运行时代码.

6. App 组件中代码

import _ from 'lodash'

function App () {
   console.log(_.chunk(['a', 'b', 'c', 'd']))
  return    <div>Test</div>
}

没有引入lodash
没有引入lodash

引入lodash
引入lodash

优化后的
优化后的