React component performance optimization best practices

React component performance optimization best practices

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

1. Clean up the components before uninstalling

The global events and timers registered for the window in the component should be cleaned up before the component is uninstalled to prevent the continued execution of the component after it is uninstalled and affect the performance of the application.

Requirement: Use useState to save a value, then start the timer to change the value, and uninstall the component to check whether the timer is still running.

Import React, {useState, useEffect} from 'REACT' ; Import ReactDOM from 'DOM-REACT' ; const App = () => { const [count, setCount] = useState( 0 ); useEffect( () => { let timer = setInterval ( () => { setCount( prev => prev + 1 ); }, 1000 ); return () => { clearInterval (timer); } }, []); return ( < button onClick = {() => ReactDOM.unmountComponentAtNode(document.getElementById('root'))}> {count} </button > ) } export default App; copy code

2. PureComponent

  1. What is a pure component

Pure components will perform 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.

  1. What is a shallow comparison

Compare whether the addresses of the reference data types in the memory are the same, and compare whether the values of the basic data types are the same.

  1. How to implement pure components

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

  1. Why not directly perform the diff operation, but perform a shallow comparison first, is there no performance cost for the shallow comparison?

Compared with the diff comparison operation, the shallow comparison will consume less performance. The diff operation will re-traverse the entire virtualDOM tree, while the shallow comparison only operates on the state and props of the current component.

Requirement: Store count as 1 in the state object. After the component is mounted, change the value of the count attribute to 1 again, and then pass the count to the pure component and the impure component respectively.

class App extends React . Component { constructor ( props ) { super (); this .state = { count : 1 } } componentDidMount () { this .setState({ count : 1 }) } render () { return ( < div > < RegularChildComponent count = { this.state.count }/ > < PureChildComponent count = { this.state.count }/ > </div > ) } } class RegularChildComponent extends React . Component { render () { console .log( 'RegularChildComponent render' ); return < div > {this.props.count} </div > } } class PureChildComponent extends React . PureComponent { render () { console .log( 'PureChildComponent render' ); return < div > {this.props.count} </div > } } export default App; copy code

Console output

RegularChildComponent render PureChildComponent render RegularChildComponent render Copy code

3. shouldComponentUpdate

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

Return true to re-render the component, return false to prevent re-rendering.

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

Requirement: Display employee information on the page. The employee information includes name, age, and position. But only the name and age are displayed on the page. That is to say, it is necessary to re-render the component only when the name and age change, if the employee s There is no need to re-render the component if other information has changed.

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 > } } Copy code

4. React.memo

memo basic use

Turn the functional component into a pure component, compare the current props with the last props in a shallow level, and prevent the component from re-rendering if the same.

Requirement: The parent component maintains two states, index and name. The timer is turned on to keep the index changing, and the name is passed to the child component to check whether the parent component has updated and 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 Copy the code

Pass comparison logic for memo

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

The first parameter of the comparison function is the props of the previous time, and the second parameter is the props of the next time. The comparison function returns true, no rendering is performed, false is returned, and the component is re-rendered.

import React, {memo, useEffect, useState} from "react" function ShowPerson ( {person} ) { console .log( "ShowPerson render..." ) return ( < div > {person.name} {person.age} </div > ) } function comparePerson ( prevProps, nextProps ) { if ( prevProps.person.name !== nextProps.person.name || prevProps.person.age !== nextProps.person.age ) { return false } return true } const ShowPersonMemo = memo(ShowPerson, comparePerson) function App () { const [person, setPerson] = useState({ name : " " , age : 20 , job : "waiter" }) useEffect( () => { setTimeout ( () => { setPerson({ ...person, job : "chef" }) }, 1000 ) }, []) return < ShowPersonMemo person = {person}/> } Export default App Copy the code

5. Use component lazy loading

Using component lazy loading can reduce the bundle file size 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 Copy the code

2. Lazy loading of components based on conditions

Suitable for components that will 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 Copy the code

6. Use Fragment to avoid extra markup

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

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

div

React fragment

import { Fragment } from "react" function App() { return ( < Fragment > < div > message a </div > < div > message b </div> </Fragment> ) } Copy code
function App() { return ( <> < div > message a </div > < div > message b </div> </> ) } Copy code

7.

After using the inline function, the render method will create an instance of the function every time it is run, causing React to compare the old and new functions when comparing the Virtual DOM. As a result, React always binds the new function to the element, and the old one. The function instance must 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 })} /> ) } } Copy code

The correct way 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}/> ) } } Copy code

8. Binding this function in the constructor

If you use fn() {} to define an event function in a class component, the event function this points to undefined by default, which means that the internal this point of the function needs to be corrected.

The this point of the function can be corrected in the constructor, or it can be corrected in-line. The two do not seem to be much different, but the impact on performance is different.

export default class App extends React . Component { constructor () { super () //Method one //The constructor is executed only once, so the corrected code pointed to by the function this is executed only once. this .handleClick = this .handleClick.bind( this ) } handleClick () { console .log( this ) } render () { //Method two //Problem: every time the render method is executed, the bind method will be called to generate a new function instance. return < button onClick = {this.handleClick.bind(this)} > button </button > } } Copy code

9. Arrow functions in class components

Using arrow functions in class components will not have this pointing problem, 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 > } } Copy code

Arrow functions have an advantage over this pointing problem, but they also have a downside.

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

10. Avoid using inline style attributes

When using inline style to add styles to an element, the inline style will be compiled into JavaScript code, and the style rules will be mapped to the element through JavaScript code. The browser will spend more time executing scripts and rendering UI, thereby increasing The rendering time of the component.

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

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

A better way is to import the CSS file into the style component. What can be done directly through CSS should not be done through JavaScript, because JavaScript is very slow to manipulate the DOM.

11. Optimize rendering conditions

Frequent mounting and unmounting of components is a very performance-consuming operation. To ensure the performance of the application, the number of mounting and unmounting components should be reduced.

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

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

In the above code, when the rendering conditions change, React internally compares the Virtual DOM and found that the first component is AdminHeader, now the first component is Header, the second component is Header, and now the second component One component is Content. If the component changes, React will uninstall AdminHeader, Header, Content, and remount Header and Content. This mounting and uninstalling is unnecessary.

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

12. Avoid repeated infinite rendering

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

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

Unlike other life cycle functions, the render method should be treated as a pure function, which means that in the render method, do not do the following things, do not call the setState method, do not use other means to query and change native DOM elements, and any other changes to the application Operation, the execution of the render method should be based on the change of the state, so that the behavior of the component can be kept consistent with the rendering mode.

13. Create error boundaries for components

By default, component rendering errors will cause the entire application to be interrupted, and creating an error boundary ensures that the application will not be interrupted when an error occurs in a specific component.

The error boundary is a React component that can capture errors that occur when child components are rendered. When an error occurs, the error can be recorded and an alternate UI interface can be displayed.

The error boundary involves two life cycle functions, namely getDerivedStateFromError and componentDidCatch.

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

The componentDidCatch method is used to record application error information, and the parameter of this method is the error object.

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/> } } Copy code
import React from "react" export default class App extends React . Component { render () { //throw new Error("lalala") return < div > App works </div > } } Copy code
import React from "react" import ReactDOM from "react-dom" import ErrorBoundaries from "./ErrorBoundaries" ReactDOM.render ( < ErrorBoundaries/> , Document .getElementById ( "the root" )) Copy the code

Note: The error boundary cannot capture asynchronous errors, such as errors that occur when a button is clicked.

14. Avoid data structure mutation

The data structure of props and state in the component should be consistent, and data structure mutations will cause inconsistent output

import React, {Component} from "react" export default class App extends Component { constructor () { super () this .state = { employee : { name : " " , 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 > ) } } Copy code

15. Add a unique identifier to the list data

When we need to render list data, we should add a key attribute to each list item, and the value of the key attribute must be unique.

The key attribute allows React to directly know which list items have changed, thus avoiding the performance cost of traversing the Virtual DOM one by one to find changes within React, and avoiding the re-creation of elements due to changes in location.

When the list item does not have a unique identifier, you can temporarily use the index as the value of the key attribute, but only if the list item is static and will not be dynamically changed, for example, the list item will not be sorted or filtered, and will not be added from the top or the middle Or delete the item. When the above behavior occurs, the index will be changed and is not reliable.

import React from "react" const arrayData = [ { id : 1 , data : "apple" }, { id : 2 , data : "pear" } ] function App () { return ( < ul > {arrayData.map(item => ( < li key = {item.id} > {item.data} </li > ))} </ul > ) } Export default App Copy the code

16. Dependency optimization

Applications often rely on third-party packages, but we don t want to reference all the code in the package, we only want to include which code is used. At this point, you can use plugins to optimize dependencies. Optimize resources

Currently we use lodash as an example, the application is created based on the create-react-app scaffolding

  1. Download dependency

    yarn add react-app-rewired customize lodash babel-plugin-lodash

    a. react-app-remired: Override the default configuration of create-react-app

    module .exports = oldConfig => { return newConfig; } //parameter oldConfig is the default webpack config copy the code

    b. 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 }) Copy code
    • override: can receive multiple parameters, each parameter is a configuration function, the function receives oldConfig, and returns newConfig
    • useBabelRc: Allow the use of .babelrc files for babel configuration

    c. babel-plugin-lodash: streamline lodash in the application

  2. Create a new project in the root directory of the project

    config-overrides.js
    And add configuration code

    const {override, useBabelRc} = require ( "customize-cra" ) Module1 .exports = the override (useBabelRc ()) copying the code
  3. modify

    package.json
    Build commands in the file

    "scripts" : { "start" : "react-app-rewired start" , "build" : "react-app-rewired build" , "test" : "react-app-rewired test --env=jsdom" , " eject" : "react-scripts eject" } Copy code
  4. create

    .babelrc
    File and add configuration

    { "plugins" : [ "lodash" ] } Copy code
  5. 3.kinds of JS files in the production environment

    1. main.[hash].chunk.js: This is your application code, App.js etc.

    2. 1.[hash].chunk.js: This is the third-party library code, including the modules you imported in node_modules

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

    Initial packet size when lodash is not added:

    Package size after adding lodash:

    The package size after optimizing lodash:

  6. 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 Copy the code