React performance optimization best practices

Keywords: Javascript React

React component performance optimization best practices

The core of React component performance optimization is to reduce the frequency of rendering real DOM nodes and Virtual DOM comparison.

1. Clean the components before unloading

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

Requirement: start the timer, then uninstall the component and check whether the timer in the component is still running.

import React, { useState, useEffect } from "react"
import ReactDOM from "react-dom"

const App = () => {
  let [index, setIndex] = useState(0)
  useEffect(() => {
    let timer = setInterval(() => {
      setIndex(prev => prev + 1)
      console.log('timer is running...')
    }, 1000)
    return () => clearInterval(timer)
  }, [])
  return (
    <button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById("root"))}>
      {index}
    </button>
  )
}

export default App

2. PureComponent

  1. What is a pure component

    Pure components will make a shallow comparison of component input data. If the current input data is the same as the last input data, the component 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 components

    Class components inherit the PureComponent class, and function components use the memo method

  4. Why not directly perform diff operation, but perform shallow comparison first. Does shallow comparison have no performance consumption

    Shallow comparisons consume less performance than diff comparisons. The diff operation will traverse the entire virtual DOM tree again, while the shallow comparison will only operate the state and props of the current component.

  5. Requirement: store the name value as Zhang San in the status object. After the component is mounted, change the value of the name attribute to Zhang San again, and then pass the name to pure components and non pure components respectively to view the results.

import React from "react"
export default class App extends React.Component {
  constructor() {
    super()
    this.state = {name: "Zhang San"}
  }
  updateName() {
    setInterval(() => this.setState({name: "Zhang San"}), 1000)
  }
  componentDidMount() {
    this.updateName()
  }
  render() {
    return (
      <div>
        <RegularComponent name={this.state.name} />
        <PureChildComponent name={this.state.name} />
      </div>
    )
  }
}

class RegularComponent extends React.Component {
  render() {
    console.log("RegularComponent")
    return <div>{this.props.name}</div>
  }
}

class PureChildComponent extends React.PureComponent {
  render() {
    console.log("PureChildComponent")
    return <div>{this.props.name}</div>
  }
}

3. shouldComponentUpdate

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

Return true to re render the component and false to prevent re rendering.

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

Requirement: display employee information in the page, including name, age and position. However, you only want to display name and age in the page. That is, it is necessary to re render the component only when the name and age change. If other employee information changes, it is not necessary to re render the component

import React from "react"

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {name: "Zhang San", age: 20, job: "waiter"}
  }
  componentDidMount() {
    setTimeout(() => this.setState({ job: "chef" }), 1000)
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.state.name !== nextState.name || this.state.age !== nextState.age) {
      return true
    }
    return false
  }

  render() {
    console.log("rendering")
    let { name, age } = this.state
    return <div>{name} {age}</div>
  }
}

4. React.memo

  1. Basic use of memo
  • Change the function component into a pure component, and make a shallow comparison between the current props and the last props. If they are the same, the component will be prevented from re rendering.

  • Requirements: the parent component maintains two statuses, index and name. Start the timer to make the index change continuously. Pass the name to the child component to check whether the parent component is updated and whether the child component is also updated.

import React, { memo, useEffect, useState } from "react"

function ShowName({ name }) {
  console.log("showName render...")
  return <div>{name}</div>
}

const ShowNameMemo = memo(ShowName)

function App() {
  const [index, setIndex] = useState(0)
  const [name] = useState("Zhang San")
  useEffect(() => {
    setInterval(() => {
      setIndex(prev => prev + 1)
    }, 1000)
  }, [])
  return (
    <div>
      {index}
      <ShowNameMemo name={name} />
    </div>
  )
}

export default App
  1. Pass comparison logic for memo

Use the memo method to customize the comparison logic for deep comparison.

The first parameter of the comparison function is the previous props, and the second parameter of the comparison function is the next props. The comparison function returns true without rendering, the comparison function returns false, and the component re renders

function App() {
  const [person, setPerson] = useState({ name: "Zhang San", age: 20, job: "waiter" })
  return <>
  <ShowPerson person={person} />
  <button onClick={() => setPerson({...person, job: "chef"})}>button</button>
  </>
}
export default App
function ShowPerson({ person }) {
  console.log("ShowPerson render...")
  return (
    <div>
      {person.name} {person.age}
    </div>
  )
}
import React, { memo, useEffect, useState } from "react"

const ShowPersonMemo = memo(ShowPerson, comparePerson)

function comparePerson(prevProps, nextProps) {
  if (
    prevProps.person.name !== nextProps.person.name ||
    prevProps.person.age !== nextProps.person.age
  ) {
    return false
  }
  return true
}
function App() {
  const [person, setPerson] = useState({ name: "Zhang San", age: 20, job: "waiter" })
  return <>
  <ShowPersonMemo person={person} />
  <button onClick={() => setPerson({...person, job: "chef"})}>button</button>
  </>
}
export default App

5. Load using components

Lazy component loading can reduce the size of bundle file and speed up component rendering

  1. Lazy loading of routing components
import React, { lazy, Suspense } from "react"
import { BrowserRouter, Link, Route, Switch } from "react-router-dom"

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

function App() {
  return (
    <BrowserRouter>
      <Link to="/">Home</Link>
      <Link to="/list">List</Link>
      <Switch>
        <Suspense fallback={<div>Loading</div>}>
          <Route path="/" component={Home} exact />
          <Route path="/list" component={List} />
        </Suspense>
      </Switch>
    </BrowserRouter>
  )
}
export default App
  1. Load components according to conditions

    Applicable to components that do not switch frequently with conditions

import React, { lazy, Suspense } from "react"

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

export default App

6. Use Fragment to avoid additional marking

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

function App() {
  return (
    <div>
      <div>message a</div>
      <div>message b</div>
    </div>
  )
}

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

In order to solve this problem, React introduced the fragment placeholder tag. Using the placeholder tag not only meets the requirement of having a common parent, but also won't add additional meaningless tags

import { Fragment } from "react"

function App() {
  return (
    <Fragment>
      <div>message a</div>
      <div>message b</div>
    </Fragment>
  )
}
function App() {
  return (
    <>
      <div>message a</div>
      <div>message b</div>
    </>
  )
}

7. 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 instance will be handed over to the garbage collector for processing

import React from "react"

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {
      inputValue: ""
    }
  }
  render() {
    return (
      <input
        value={this.state.inputValue}
        onChange={e => this.setState({ inputValue: e.target.value })}
        />
    )
  }
}

The correct approach is to define the function separately in the component and bind the function to the event

import React from "react"

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {
      inputValue: ""
    }
  }
  setInputValue = e => {
    this.setState({ inputValue: e.target.value })
  }
  render() {
    return (
      <input value={this.state.inputValue} onChange={this.setInputValue} />
    )
  }
}

8. Bind the function this in the constructor

In class components, if the function is defined in fn() {}, the function this points to undefined by default. That is, the this point inside the function needs to be corrected

this of the function can be corrected in the constructor or in the line. They don't look very different, but they have different impact on performance

export default class App extends React.Component {
   constructor() {
    super()
     // Mode 1
     // The constructor is executed only once, so the function this points to the corrected code is executed only once
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    console.log(this)
  }
  render() {
    // Mode II 
    // Problem: every time the render method executes, it will call the bind method to generate a new function instance
    return <button onClick={this.handleClick.bind(this)}>Button</button>
  }
}

9. Arrow function in class component

Using the arrow function in the class component will not have the problem of this pointing, because the arrow function itself does not bind this

export default class App extends React.Component {
  handleClick = () => console.log(this)
  render() {
    return <button onClick={this.handleClick}>Button</button>
  }
}

Arrow function has advantages in this pointing problem, but it also has disadvantages

When using the arrow function, the function is added as the instance object attribute of the class rather than the prototype object attribute. If the component is reused multiple times, there will be the same function instance in each component instance object, which reduces the reusability of the function instance and causes a waste of resources

To sum up, the best way to correct this point inside a function is to use the bind method in the constructor

10. Avoid using inline style attributes

When you use inline style to add styles to elements, inline style will be compiled into JavaScript code. By mapping style rules to elements through JavaScript code, the browser will spend more time executing scripts and rendering UI, thus increasing the rendering time of components

function App() {
  return <div style={{ backgroundColor: "skyblue" }}>App works</div>
}

In the above component, the inline style is attached to the element. The added inline style is a JavaScript object. The backgroundColor needs to be converted into an equivalent CSS style rule, and then applied to the element, which involves the execution of the script

A better way is to import CSS files into style components. What can be done directly through CSS should not be done through JavaScript, because JavaScript is very slow to operate DOM

11. Optimize rendering conditions

Frequent component mounting and unloading is a performance consuming operation. In order to ensure the performance of the application, the number of component mounting and unloading should be reduced

In React, we often render different components according to conditions. Conditional rendering is a necessary optimization operation

function App() {
  if (true) {
    return (
      <>
        <AdminHeader />
        <Header />
        <Content />
      </>
    )
  } else {
    return (
      <>
        <Header />
        <Content />
      </>
    )
  }
}

In the above code, when the rendering conditions change, the Virtual DOM comparison inside React finds that the first component is AdminHeader just now, the first component is Header just now, the second component is Header now, and the second component is Content now. When the component changes, React will uninstall AdminHeader, Header and Content, and re mount Header and Content, This kind of mounting and unloading is unnecessary

function App() {
  return (
    <>
      {true && <AdminHeader />}
      <Header />
      <Content />
    </>
  )
}

12. 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, the render method will be recursively called, resulting in an application error

export default class App extends React.Component {
  constructor() {
    super()
    this.state = {name: "Zhang San"}
  }
  render() {
    this.setState({name: "Li Si"})
    return <div>{this.state.name}</div>
  }
}

Unlike other lifecycle functions, the render method should be treated as a pure function. This means that in the render method, do not do the following things, such as do not call the setState method, do not use other means to query and change native DOM elements, and any other operations to change the application. The execution of the render method should be based on the change of state, This keeps the behavior of the component consistent with the rendering method

13. Create error boundaries for components

By default, component rendering errors will break the whole application. Creating error boundaries can ensure that the application will not break when errors occur in specific components

Error boundary is a React component, which can capture the errors of child components during rendering. When errors occur, they can be recorded and the standby UI interface can be displayed

The error boundary involves two lifecycle functions, 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. The parameter of this method is the error object

// ErrorBoundaries.js
import React from "react"
import App from "./App"

export default class ErrorBoundaries extends React.Component {
  constructor() {
    super()
    this.state = {
      hasError: false
    }
  }
  componentDidCatch(error) {
    console.log("componentDidCatch")
  }
  static getDerivedStateFromError() {
    console.log("getDerivedStateFromError")
    return {
      hasError: true
    }
  }
  render() {
    if (this.state.hasError) {
      return <div>An error has occurred</div>
    }
    return <App />
  }
}
// App.js
import React from "react"

export default class App extends React.Component {
  render() {
    // throw new Error("lalala")
    return <div>App works</div>
  }
}
// index.js
import React from "react"
import ReactDOM from "react-dom"
import ErrorBoundaries from "./ErrorBoundaries"

ReactDOM.render(<ErrorBoundaries />, document.getElementById("root"))

Note: error boundaries cannot capture asynchronous errors, such as those that occur when a button is clicked

14. Avoid sudden change of 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 React, { Component } from "react"

export default class App extends Component {
  constructor() {
    super()
    this.state = {
      employee: {
        name: "Zhang San",
        age: 20
      }
    }
  }
  render() {
    const { name, age } = this.state.employee
    return (
      <div>
        {name}
        {age}
        <button
          onClick={() =>
            this.setState({
              ...this.state,
              employee: {
                ...this.state.employee,
                age: 30
              }
            })
          }
        >
          change age
        </button>
      </div>
    )
  }
}

15. Dependency optimization

Applications often rely on third-party packages, but we don't want to refer to all the codes in the package. We just include the codes we want to use. At this time, we can use plug-ins to optimize the dependencies Optimize resources

Now let's use lodash as an example. The application is created based on the create react app scaffold.

  1. Download dependency yarn add react app rewired customize CRA lodash Babel plugin lodash

React app Rewired: overrides the default configuration of create react app

module.exports = function (oldConfig) {
  return newConfig
}
// The oldConfig in the parameter is the default webpack config

Customize CRA: some auxiliary methods are exported 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 receives oldConfig and returns newConfig

useBabelRc: allow babel configuration using. babelrc file

Babel plugin lodash: streamline lodash in applications

  1. Create a new config-overrides.js in the root directory of the project and add the configuration code
const { override, useBabelRc } = require("customize-cra")

module.exports = override(useBabelRc())
  1. Modify the build command in the package.json file
"scripts": {
 "start": "react-app-rewired start",
 "build": "react-app-rewired build",
 "test": "react-app-rewired test --env=jsdom",
 "eject": "react-scripts eject"
}
  1. Create a. babelrc file and add the configuration
{
 "plugins": ["lodash"]
}
  1. 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 a third-party library, Include you in node_modules Modules imported in

3. runtime~main.[hash].js webpack Runtime code

  1. App components
import React from "react"
import _ from "lodash"

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

export default App

summary

The above is the best practice of code performance optimization in the process of React development. Of course, it can also be optimized in other aspects, such as dividing code blocks through webpack splitChunks, compressing JS and CSS, reducing the search process, caching cache, webpack bundle analyzer construction analysis, etc., and using CDN and HTTP cache to enable Gzip compression, There are many ways to optimize front-end performance, and there are still a lot of knowledge to learn.

Posted by zander213 on Tue, 21 Sep 2021 23:21:15 -0700