react performance optimization

react performance optimization

The core of React component performance optimization is to reduce the frequency of rendering real DOM nodes and reducing the frequency of Virtual DOM comparison, so as to improve performance

1. Clean the components before unloading

The global events and timers registered for window in the component should be cleared before the component is unloaded to prevent the continued execution of the component after unloading from affecting the application performance

We start a timer and then uninstall the component to see if the timer in the component is still running. Test component starts a timer

import {useEffect} from 'react'

export default function Test () {
  useEffect(() => {
    setInterval(() => {
      console.log('The timer starts executing')
    }, 1000)
  }, [])
  return <div>Test</div>
}

The timer component is introduced into App.js, and then the flag variable is used to control the rendering and unloading of components

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

export default App;

In the browser, we click the button to find that the timer is still running after the component is unloaded. In this way, too many components or the constant rendering and unloading of the component will start a lot of timers, and the performance of our application will be collapsed. Therefore, we need to destroy the timer when building and unloading.

import {useEffect} from 'react'

export default function Test () {
  useEffect(() => {
    // To destroy the timer, we need to use a variable to accept the timer id
    const InterValTemp =  setInterval(() => {
      console.log('The timer starts executing')
    }, 1000)
    return () => {
      console.log(`ID by ${InterValTemp}The timer was destroyed`)
      clearInterval(InterValTemp)
    }
  }, [])
  return <div>Test</div>
}

At this time, when we click destroy build, the timer is destroyed

2. Class components use pure components to improve the build performance. PureComponent

1. What is a pure component

​ The pure component will perform a shallow comparison on the input data of the build. If the input data is the same as the last input data, the build will not be re rendered

2. What is shallow comparison

​ Compare whether the reference address of the reference data type in memory is the same, and compare whether the value of the basic data type is the same

3. How to implement pure component

​ Class components integrate the PureComponent class, and function components use the memo method

4. Why not directly perform diff operation, but shallow comparison? Is shallow comparison so difficult that there is no performance consumption

​ Compared with diff comparison, shallow comparison has less performance. Diff operation will traverse the whole virtual DOM tree again, while shallow comparison only compares the current component state and props

Store a with the name Zhang San in the status. After the build is mounted, we change the value of the name to Zhang San every 1 second, and then we look at pure components and non pure components to see the results

// Pure component
import { PureComponent } from 'react'
class PureComponentDemo extends PureComponent {
  render () {
    console.log("Pure component")
    return <div>{this.props.name}</div>
  }
}
// Impure component
import { Component } from 'react'
class ReguarComponent extends Component {
 render () {
   console.log("Impure component")
   return <div>{this.props.name}</div>
 }
}

Introduce pure components and non pure components, and start the timer after the component is hung. Change the value of name to Zhang San every 1 second

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

Open the browser to view the execution results

We found that pure components are only executed once. When changing the same value later, components are not re rendered, while non pure components are re rendered every time. Therefore, pure components save more performance than non pure components

3. Function component to realize pure component memo

  1. Basic use of memo

    Turn the function component into a pure component, and compare the current props with the previous props in a shallow level. If they are the same, the rendering of the component will be.

We maintain two statuses in the parent component. Index and name start the timer to make the index change constantly. Name is passed to the child component to check whether the parent component is updated and whether the child component is also updated. We don't need memo to check the results first

import { useState, useEffect } from 'react'
function App () {
  const [ name ] = useState("Zhang San")
  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("Component updated")
  return <div>{name}</div>
}

Open the browser to view the execution results

Without using memo to turn a function component into a pure component, we find that the child component is re rendered with the update of the parent component, but the value it depends on is not updated, which wastes performance. We use memo to avoid unnecessary updates

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

const ShowName = memo(function ShowName ({name}) {
  console.log("Component updated")
  return <div>{name}</div>
})

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

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

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

We open the browser again to view the execution results

Now the sub components of index change are not re rendered. After changing the component into a pure component with memo, the dependent values are not updated but are updated with the parent component

4. Function components to implement pure components (pass custom comparison logic for memo methods)

The memo method is also a shallow comparison

The memo method has a second parameter, which is a function

This function has two parameters. The first parameter is the previous props and the second parameter is the next props

This function returns false for re rendering and true for re rendering

For example, we have the employee name and position data, but only the employee name is used in the page, so we only need to observe whether the employee name has changed, so we compare whether it needs to be re rendered in the second parameter of 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("Component updated")
  return <div>{person.name}</div>
}, compare)

function App () {
  const [ person, setPerson ] = useState({ name: "Zhang San", job: "engineer"})

  useEffect(() => {
    setInterval (() => {
      setPerson({
        ...person,
        job: "transport human manure"
      })
    }, 1000)
  }, [person])

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

5. shouldComponentUpdata

Pure components can only be used for shallow comparison. To make deep comparison, use shouldComponentUpdate, which is used to write custom comparison logic

Return true to re render the component, and return false to re render the component

The first parameter of the function is nextProps and the second parameter is NextState

For example, we have two data: employee name and position, but only employee name is used in the page. We only need to observe whether the employee name has changed. Use shouldComponentUpdata to control that the component can be re rendered only when the employee name has changed, Let's look at the difference between using the shouldComponentUpdata lifecycle function and not using the shouldComponentUpdata lifecycle function

// Components not used
import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      person: {
        name: 'Zhang San',
        job: 'engineer'
      }
    }
  }
  componentDidMount (){
    setTimeout (() => {
      this.setState({
        person: {
          ...this.state.person,
          job: "Repair water pipe"
        }
      })
    }, 2000) 
  }
  render () {
    console.log("render Method is executed")
    return <div>
      {this.state.person.name}
    </div>
  }
}

We open the browser and wait two seconds

It was found that the render method was executed twice and the component was re rendered, but we did not change the name attribute, so this wasted performance. We used the shouldComponentUpdata lifecycle function to judge whether the name had changed

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      person: {
        name: 'Zhang San',
        job: 'engineer'
      }
    }
  }
  componentDidMount (){
    setTimeout (() => {
      this.setState({
        person: {
          ...this.state.person,
          job: "Repair water pipe"
        }
      })
    }, 2000) 
  }
  render () {
    console.log("render Method is executed")
    return <div>
      {this.state.person.name}
    </div>
  }
  shouldComponentUpdate (nextProps, nextState) {
    if (this.state.person.name !== nextState.person.name) {
      return true;
    }
    return false;
  }
}

We open the browser again and wait for two seconds

When we only change the job, the render method is executed only once, which reduces unnecessary rendering and saves performance

6. Load using components

Using lazy load routing can reduce the bundle file size and speed up the build rendering

Create Home build

// Home.js
function Home() {
  return (
    <div>
      home page
    </div>
  )
}

export default Home

Create List component

// List.js
function List() {
  return (
    <div>
      List page
    </div>
  )
}

export default List

Browserouter, route, switch, link, home and list are introduced from the react router DOM package to create routing rules, switching areas and jump buttons

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

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

use Lazy, suspend to create the loading area and loading function

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="/">home page</Link>
        <Link to="/list">List page</Link>
      <Switch>
        <Suspense fallback={<Loading />}>
          <Route path="/" exact component={Home}></Route>
          <Route path="/list" component={List}></Route>
        </Suspense>
      </Switch>
    </BrowserRouter>
  </div>
}

Use annotation to name the packaged file

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

7. Load components according to conditions

Applicable to components that do not switch frequently with conditions

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;

This will only load one component to improve performance

8. Improve the rendering performance of React components by using placeholder markers

If the jsx returned in the React component has multiple sibling elements, it must have a common parent

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

In order to meet this condition, we usually add a div outside, but in this way, there will be an additional meaningless tag. If each element has multiple such meaningless tags, the burden of the browser rendering engine will increase

In order to solve this problem, React introduced the fragment placeholder tag. Using placeholder editing not only meets the requirements of common parents, but also does not render a meaningless tag

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

Of course, the fragment tag is still too long, so there is a shorthand method

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

9. Do not use inline function definitions

After using the inline function, the render method will create a new instance of the function every time it runs, resulting in the unequal comparison between the old and new functions during the Virtual DOM comparison of React. As a result, React always binds a new function instance for the element, and the old function has to be handed over to the garbage collector

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: 'Zhang San'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={() => { this.setState({name: "Li Si"})}}>modify</button>
    </div>
  }
}


export default App;

Change to the following method

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: 'Zhang San'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={this.setChangeName}>modify</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "Li Si"})
  }
}

10. Bind the function this in the constructor

In class components, if you define a function in fn() {}, the this point of the function is only undefined by default, that is, the this point inside the function needs to be corrected,

this correction can be made to the function in the constructor or internally. They don't seem to be much different, but the impact on performance is different

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: 'Zhang San'
    }
    // In this way, the constructor will execute only once, so it will execute only once
    this.setChangeName = this.setChangeName.bind(this)
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      {/* In this way, a new function instance will be generated when the render method is executed */}
      <button onClick={this.setChangeName.bind(this)}>modify</button>
    </div>
  }
  setChangeName() {
    this.setState({name: "Li Si"})
  }
}

Correcting this point in the constructor will only be corrected once, while in the render method, if this point is not corrected, it will be undefined. However, if it is corrected in the render method, each execution of the render method will return a new function instance, which has an impact on performance

11. Arrow function in class component

There is no this pointing problem when using arrow functions in class components, because arrow functions do not bind this

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: 'Zhang San'
    }
  }
  render () {
    return <div>
      <h3>{this.state.name}</h3>
      {/* <button onClick={() => { this.setState({name: "Li Si "})}} > Modify < / button > */}
      <button onClick={this.setChangeName}>modify</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "Li Si"})
  }
}

Arrow functions do have advantages in this direction

However, when the arrow function is used as a member in a class component, it will be added as an instance object attribute rather than a prototype object attribute. If the component is reused many times, each component instance will have the same function instance, which reduces the availability of the function instance and causes a waste of resources

To sum up, we conclude that when using class components, it is recommended to correct the problem of this pointing by using the bind method in the constructor

12. Avoid using inline style attributes

When the inline style is used, the inline style will be compiled into JavaScript code. The style rules will be mapped to the elements through JavaScript code, and the browser will spend more time executing scripts and rendering UI, thus increasing the rendering time of components

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

In the above components, the background color of the element is red. This style is a JavaScript object. The background color needs to be converted into an equivalent css rule and then applied to the element. This involves the execution of the script. In fact, the problem of inline style is to add a style to the element during execution, rather than adding a style to the element during compilation

A better way is to import style files. Do not use JavaScript to do things that can be done directly through css, because JavaScript is very slow to operate DOM

13. Optimize conditional rendering to improve component performance

Frequently attaching and uninstalling components is a very performance consuming thing. You should reduce the times of attaching and uninstalling components,

In React, we often render different components through different conditions. Conditional rendering is an optimization operation that must be done

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

When the conditions in the above code are different, the first element and the second element have been changed during the Virtual DOM comparison in React. Therefore, component 1, component 2 and component 3 will be unloaded, and then component 2 and component 3 will be rendered. In fact, only component 1 has changed. It is not necessary to re hang on component 2 and component 3

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

In this way, only component 1 changes, saving unnecessary rendering

16. Avoid repeated infinite rendering

When the application state changes, React will call the render method. If you continue to change the application state in the render method, a recursive call will occur, resulting in an application error

Uncapped error: maximum update depth exceeded. This can happen when a component repeatedly calls setState within componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops. The maximum number of times react limits is 50

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      name: 'Zhang San'
    }
  }
  render () {
    this.setState({name:"Zhang Wu"})
    return <div>
      <h3>{this.state.name}</h3>
      <button onClick={this.setChangeName}>modify</button>
    </div>
  }
  setChangeName = () => {
    this.setState({name: "Li Si"})
  }
}

Unlike other lifecycle functions, the render method should be treated as a pure function, which means that you should not do the following in the render method

  1. Do not call the setState method to change the state
  2. Do not use other means to query and change DOM elements, as well as other operations to change the application
  3. Do not call the setState method repeatedly in the componentWillUpdate lifecycle to change the state
  4. Do not call the setState method repeatedly in the componentDidUpdate lifecycle to change the state

The render method executes according to the state change, which can keep the behavior of the component consistent with the rendering method

15. Create error boundaries for components

By default, component rendering errors will interrupt the entire application. Creating an error boundary can ensure that the application will not be interrupted when a component error occurs. The error boundary is a React component that can capture the errors of child components during rendering. When an error occurs, it can be recorded and the standby UI interface can be displayed,

The error boundary involves two life cycles: getDerivedStateFromError and componentDidCatch

getDerivedStateFromError is a static method, which needs to return an object, which will be merged with the state object to change the application state

The componentDidCatch method is used to record application error information, and the method returns the error object

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>
        An error has occurred
      </div>
    }
    return <Test></Test>
  }
}

class Test extends Component {
  constructor () {
    super()
    this.state = {
      hanError: false
    }
  }
  render () {
    throw new Error("An error has occurred");
    return <div>
      correct
    </div>
  }
}

When we throw an error, getDerivedStateFromError will merge the returned object to state, so hasError will become true and render our standby interface

Note: getDerivedStateFromError cannot capture asynchronous errors, such as errors after a button click event

16. Avoid sudden changes in data structure

The data structures of props and state in the component should be consistent. Sudden changes in the data structure will lead to inconsistent output

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      man: {
        name: "Zhang San",
        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}>modify</button>
    </div>
  }
  setMan () {
    this.setState({
      ...this.state,
      man: {
        name: "Li Si"
      }
    })
  }
}

At first glance, there seems to be no problem with this code. After a closer look, we find that the age field is lost after we modify the name. Because the data is mutated, we should avoid such data mutation

import { Component } from 'react'
class App extends Component {
  constructor () {
    super()
    this.state = {
      man: {
        name: "Zhang San",
        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}>modify</button>
    </div>
  }
  setMan () {
    this.setState({
      man: {
        ...this.state.man,
        name: "Li Si"
      }
    })
  }
}

17. Dependency optimization

We often use three-party packages in our applications, but we don't want to reference all the code in the package. We only want to use those codes and include them. At this time, we can use plug-ins to optimize dependencies

We use lodash as an example. The application is created based on the create react app scaffold

1. Download dependency

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

React app Rewired: overrides the create react app configuration

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

Customize CRA: export auxiliary methods to make the above writing more concise

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

override: multiple parameters can be received. Each parameter is a configuration function. The function accepts oldConfig and returns newConfig

useBabelRc: allows babel configuration using. babelrc files

Babel plugin lodash: streamline lodash

2. Create a new config-overrides.js in the root directory of the project and add the following configuration

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

module.exports = override(useBabelRc())

3. Modify the build command in the package.json file

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

4. Create a. babelrc file and add the configuration

{
  "plugins": ["lodash"]
}

5. Three JS files in production environment

  1. main.[hash].chunk.js: This is your application code, App.js, etc
  2. 1.[hash].chunk.js: This is the code of the third-party library, which is included in your node_modules
  3. runtime~main.[hash].js: webpack runtime code

6. Code in app component

import _ from 'lodash'

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

lodash is not introduced

Introducing lodash

Optimized

Posted by rugzo on Thu, 02 Dec 2021 14:34:35 -0800