I am a heavy react fan. In my spare time, I have read many React articles and written many REACT projects. Next, I will discuss the main direction of react performance optimization and some tips in work. Send roses, hand left fragrance, reading friends can give the author like, concern a wave. Keep updating front end articles.

This article has a long length, and will be discussed from the compilation stage -> routing stage -> rendering stage -> detail optimization -> state management -> massive data source, long list rendering direction respectively.

One can not lose on the starting line, optimize the Babel configuration,webpack configuration for items

1 pain points in real projects

When we used create-react-app or Webpack to build the React project, did we ever think that our configuration could make our project faster to build, smaller project volume, and more concise and clear project structure? As we grew, projects rely on more and more, the project structure became more complex, project volume will be more and more big, build more and long time, the passage of time will become a big, heavy items, so we must learn to properly for the project ‘burden, make project can’t lose at the starting line.

2 An old project

Take an old React project that we worked on before. Instead of USING DVA and UMI to quickly build react, we used scaffolding of the old version of React. For such an old React project, all the problems mentioned above will exist. Let’s take a look.

Let’s look at the project structure first.

Take a look at build times.

To make it easier for you to see the build time, I’ve simply written a webpack, plugin ConsolePlugin, which records the time of Webpack during a compilation.

const chalk = require('chalk') /* Console color */
var slog = require('single-line-log'); /* Print console */ in a single line

class ConsolePlugin {
    constructor(options){
       this.options = options
    }
    apply(compiler){
        /** * Monitor file change Records the current changed file */
        compiler.hooks.watchRun.tap('ConsolePlugin'.(watching) = > {
            const changeFiles = watching.watchFileSystem.watcher.mtimes
            for(let file in changeFiles){
                console.log(chalk.green('Currently changed file:'+ file))
            }
        })
        /** * before a new compilation is created. Compilation begins. * /
        compiler.hooks.compile.tap('ConsolePlugin'.() = >{
            this.beginCompile()
        })
        /** * Executed when the compilation has completed. One compilation completed. * /
        compiler.hooks.done.tap('ConsolePlugin'.() = >{
            this.timer && clearInterval( this.timer )
            const endTime =  new Date().getTime()
            const time = (endTime - this.starTime) / 1000
            console.log( chalk.yellow('Compile done'))console.log( chalk.yellow('Compile time:' + time + '秒'))})}beginCompile(){
       const lineSlog = slog.stdout
       let text  = 'Start compiling:'
       /* Record start time */
       this.starTime =  new Date().getTime()
       this.timer = setInterval(() = >{
          text +=  '█'
          lineSlog( chalk.green(text))
       },50)}}Copy the code

Build times are as follows:

Packed volume:

3 retrofit old projects

For the old React project above, we started to optimize it accordingly. Since this article is mainly about React, we won’t give too much space to WebPack optimizations.

① Include or exclude limits the loader range.

{
    test: /\.jsx? $/,
    exclude: /node_modules/,
    include: path.resolve(__dirname, '.. /src'),
    use: ['happypack/loader? id=babel']
    // loader: 'babel-loader'
}
Copy the code

② Happypack multiprocess compilation

In addition to the above changes, in the plugin

/* Multithreaded compilation */
new HappyPack({
    id:'babel'.loaders: ['babel-loader? cacheDirectory=true']})Copy the code

③ Cache Babel compiled files

loaders:['babel-loader? cacheDirectory=true']
Copy the code

Tree Shaking removes redundant code

⑤ Load on demand and introduce on demand.

Optimized project structure

Optimized build times are as follows:

At a timecompilationTime was optimized from 23 seconds to 4.89 seconds

Optimized packaging volume:

Therefore, if we build React by hand, some optimization techniques are particularly important.

Thinly minded thoughts on a UI library like antD

When we do react projects, we use a UI library like ANTD, and one of the things to think about is, if we just use a single component from ANTD, like , we need to bring the whole style library in, and when you package it up, the volume is a lot bigger because of the whole style. We can introduce it on demand with.babelrc.

Thin body before

Babelrc adds the introduction of antD styles on demand.

["import", {
    "libraryName":
    "antd"."libraryDirectory": "es"."style": true
}]
Copy the code

After the thin body

conclusion

If you want to optimize the React project, it is essential to start from the build. We need to focus on every step of the way, from build to package.

Route lazy load, route listener

The react route is lazy loading, which is summarized by the author after reading the dynamic asynchronous loading component in the DVA source code. There are many pages in a large project. When configuring the route, if the route is not processed, a large number of routes will be loaded at once, which is very unfriendly to the page initialization and will prolong the page initialization time. So we want to use asyncRouter to load the page route on demand.

Traditional routing

If we do not use umi and need to manually configure the route, maybe the route will be configured like this.

<Switch>
    <Route path={'/index'} component={Index} ></Route>
    <Route path={'/list'} component={List} ></Route>
    <Route path={'/detail'} component={ Detail } ></Route>
    <Redirect from='/ *' to='/index' />
</Switch>
Copy the code

Alternatively, you can use the list to save routing information for route interception and route menu configuration.

const router = [
    {
        'path': '/index'.'component': Index
    },
    {
        'path': '/list'', 'component': List }, { 'path':'/detail', 'component': Detail }, ]Copy the code

AsyncRouter lazily loads routes and implements route listening

The react lazy load we’re talking about today is based on the import function. As you know, the import execution returns a Promise as a means of asynchronous loading. We can take advantage of this to implement react asynchronous load routing

Okay, a word doesn’t fit the code…

code

const routerObserveQueue = [] /* Store the routing satellite hook */
/* Lazily loading route guard hooks */
export const RouterHooks = {
  /* Before the routing component loads */
  beforeRouterComponentLoad: function(callback) {
    routerObserveQueue.push({
      type: 'before',
      callback
    })
  },
  /* After the routing component is loaded */
  afterRouterComponentDidLoaded(callback) {
    routerObserveQueue.push({
      type: 'after',
      callback
    })
  }
}
/* Route lazy load HOC */
export default function AsyncRouter(loadRouter) {
  return class Content extends React.Component {
    constructor(props) {
      super(props)
      /* Trigger the hook function before each route is loaded */
      this.dispatchRouterQueue('before')
    }
    state = {Component: null}
    dispatchRouterQueue(type) {
      const {history} = this.props
      routerObserveQueue.forEach(item= > {
        if (item.type === type) item.callback(history)
      })
    }
    componentDidMount() {
      if (this.state.Component) return
      loadRouter()
        .then(module= > module.default)
        .then(Component= > this.setState({Component},
          () = > {
            /* Trigger the hook function after each route is loaded */
            this.dispatchRouterQueue('after')}}))render() {
      const {Component} = this.state
      return Component ? <Component {
      . this.props} / > : null}}}Copy the code

AsyncRouter ()=>import(); then when the external Route loads the current component, load the real component in the componentDidMount lifecycle function, and render the component. We can also write to customize their own routing lazy loading state routing listener beforeRouterComponentLoad and afterRouterComponentDidLoaded, similar watch $route function in vue. Let’s see how to use it.

use

import AsyncRouter ,{ RouterHooks }  from './asyncRouter.js'
const { beforeRouterComponentLoad} = RouterHooks
const Index = AsyncRouter(() = >import('.. /src/page/home/index'))
const List = AsyncRouter(() = >import('.. /src/page/list'))
const Detail = AsyncRouter(() = >import('.. /src/page/detail'))
const index = () = > {
  useEffect(() = >{
    /* Add the listener function */  
    beforeRouterComponentLoad((history) = >{
      console.log('Currently active route is',history.location.pathname)
    })
  },[])
  return <div >
    <div >
      <Router  >
      <Meuns/>
      <Switch>
          <Route path={'/index'} component={Index} ></Route>
          <Route path={'/list'} component={List} ></Route>
          <Route path={'/detail'} component={ Detail } ></Route>
          <Redirect from='/ *' to='/index' />
       </Switch>
      </Router>
    </div>
  </div>
}
Copy the code

The effect

The react-router does not have a listener function to listen for changes in the current route.

Three controlled component granulation, independent request service rendering unit

Controllable component granulation, independent request service rendering unit is the author’s experience in practical work. The goal is to avoid global rerendering due to its own render updates or side effects.

1 Granulated control controllability component

The difference between a controllable component and a non-controllable component is whether the DOM element value is controlled by the React data state. Once the react state controls the data state, such as the input input box value, such a scene will be created. In order to make the input value change in real time, the state will be set constantly, and the render function will be triggered constantly. If the parent component is simple, it is ok, but if the parent component is complex, it will cause a whole situation. If other subcomponents componentWillReceiveProps the side-effects of hook, so the butterfly effect caused by imagination. Here’s the demo.

class index extends React.Component<any.any>{
    constructor(props){
        super(props)
        this.state={
            inputValue:' '
        }
    }
    handerChange=(e) = > this.setState({ inputValue:e.target.value  })
    render(){
        const { inputValue } = this.state
        return <div>{/* we add three subcomponents */}<ComA />
            <ComB />
            <ComC />
            <div className="box" >
                <Input  value={inputValue}  onChange={ (e) = > this.handerChange(e) } />
            </div>} {new Array(10).fill(0).map((item,index)=>{console.log(' list loops') return<div key={index} >{item}</div>})} {/ * * here may be more complex structure / / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /}</div>}}Copy the code

Component A

function index(){
    console.log('Component A render')
   return <div>This is component A</div>
}

Copy the code

Component B, there is a componentWillReceiveProps hook

class Index extends React.Component{
    constructor(props){
        super(props)
    }
    componentWillReceiveProps(){
        console.log('componentWillReceiveProps execution')
        /* May do some SAO operation wu Lian */
    }
    render(){
        console.log('Component B Render')
        return <div>This is component B</div>}}Copy the code

Component C has a list loop

class Index extends React.Component{
    constructor(props){
        super(props)
    }

    render(){
        console.log('Component C rendering')
        return <div>I'm component C {new Array(10).fill(0).map((item,index)=>{console.log(' component C list lolooped ') return<div key={index} >{item}</div>})}</div>}}Copy the code

The effect

When we put something in the input. As you can see, everything that shouldn’t have been updated is being re-implemented, which is a huge performance cost. The side effects of a setState trigger are incalculable, with a huge stream of potentially deeper updates from this component to subcomponents. So we can think about granulating this controlled component and having the update -> render process itself scheduled.

Let’s go ahead and granulate the input form above separately.

const ComponentInput = memo(function({ notifyFatherChange }:any){
    const [ inputValue , setInputValue ] = useState(' ')
    const handerChange = useMemo(() = > (e) = > {
        setInputValue(e.target.value)
        notifyFatherChange && notifyFatherChange(e.target.value)
    },[])
    return <Input   value={inputValue} onChange={ handerChange} / >
})
Copy the code

At this time, the component update is controlled by the component unit itself, and the parent component is not required to update, so the parent component does not need to set independent state to retain the state. You just need to bind to this. Not all states should be placed in the component’s state. For example, cache data. If the component needs to respond to its changes, or if the data needs to be rendered into the view, it should be put in state. This prevents unnecessary data changes that cause the component to be rerendered.

class index extends React.Component<any.any>{   
    formData :any = {}
    render(){
        return <div>{/* we add three subcomponents */}<ComA />
            <ComB />
            <ComC />
            <div className="box" >
               <ComponentInput notifyFatherChange={ (value) = >{ this.formData.inputValue = value } }  />
               <Button onClick={()= >Console. log(this.formdata)} > Print the data</Button>
            </div>} {new Array(10).fill(0).map((item,index)=>{console.log(' list loops') return<div key={index} >{item}</div>})} {/ * * here may be more complex structure / / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /}</div>}}Copy the code

The effect

This ensures that no render ripple is received anywhere other than the current component, which is what we want.

Set up a separate request rendering unit

Apply colours to a drawing unit, set up independent request directly to understand is that if we put the page is divided into the request data display part (by calling the backend interface, to get the data), and the base part (do not need to request data, has written) directly, for some logical data display part of interaction is not very complicated, I recommend using an independent component, independent request data, Controls the rendering mode independently. As to why we can analyze slowly.

First, let’s look at the traditional page pattern.

The page has three display areas, three requests are made, three setstates are triggered, and the page is rendered three times. Even if promise. all and other methods are used, there is no guarantee that part of the display area will be able to pull data again in the following interaction. Once one region pulls the data, the other two regions will also be affected. This effect is inevitable. Even though React has a good DDIFF algorithm to coordinate the same nodes, in cases like long lists, loops are inevitable.

class Index extends React.Component{
    state :any={
        dataA:null.dataB:null.dataC:null
    }
    async componentDidMount(){
        /* Obtain data from area A */
        const dataA = await getDataA()
        this.setState({ dataA })
        /* Retrieve data from area B */
        const dataB = await getDataB()
        this.setState({ dataB })
        /* Retrieve data from C */
        const dataC = await getDataC()
        this.setState({ dataC })
    }
    render(){
        const { dataA , dataB , dataC } = this.state
        console.log(dataA,dataB,dataC)
        return <div>
            <div>{/* render with dataA data */}</div>
            <div>{/* render with dataB data */}</div>
            <div>{/* display rendering with dataC data */}</div>
        </div>}}Copy the code

Next, we extracted each part into a separate rendering unit, each component requesting data to a separate rendering.

function ComponentA(){
    const [ dataA, setDataA ] = useState(null)
    useEffect(() = >{
       getDataA().then(res= > setDataA(res.data)  )
    },[])
    return  <div>{/* render with dataA data */}</div>
} 

function ComponentB(){
    const [ dataB, setDataB ] = useState(null)
    useEffect(() = >{
       getDataB().then(res= > setDataB(res.data)  )
    },[])
    return  <div>{/* render with dataB data */}</div>
} 

function ComponentC(){
    const [ dataC, setDataC ] = useState(null)
    useEffect(() = >{
       getDataC().then(res= > setDataC(res.data)  )
    },[])
    return  <div>{/* display rendering with dataC data */}</div>
}  

function Index (){
    return <div>
        <ComponentA />
        <ComponentB />
        <ComponentC />
    </div>
}
Copy the code

In this way, each other’s data updates will not affect the other.

conclusion

Split the small components that need to call the back-end interface separately, and build independent data requests and renderings. The components that depend on data update -> view rendering can be extracted from the whole system. The benefits I summarized are as follows:

(1) Avoid redundant rendering of the parent component. React is data-driven and depends on changes in state and props. Changing the state will automatically call the render function of the component. Performance has to be affected, so if you take out many components that depend on requests, you can directly reduce rendering times.

2. You can optimize the performance of components themselves, whether they are stateful components declared from class or stateless components declared from fun. You can use shouldupDate, useMemo useCallback, hooks, etc. Customizing renderers to fit the scene allows the data-dependent request component to form its own small, appropriate rendering environment.

3 can work better with Redux, and redux-action, DVA, which is derived from Redux. Components wrapped in Connect can update themselves according to the required data updates through a contract. This pattern is applied to small, data-driven components. It will make the best use of things.

Four shouldComponentUpdate,PureComponent and React.memo, immeTable.js facilitate performance tuning

Here we take immetable.js as an example, and talk about the most traditional limited update method. Part 6 will cover some details to avoid rerendering.

1 PureComponent and React. Memo

React.purecomponent implements shouldComponentUpate() by using a light contrast between props and state. If the object contains complex data structures (such as objects and arrays), it will be slightly compared, and if there are deep changes, it will be impossible to judge. React.PureComponent considers that there are no changes, and there is no render attempt.

In this case

class Text extends React.PureComponent<any.any>{
    render(){
        console.log(this.props)
        return <div>hello,wrold</div>}}class Index extends React.Component<any.any>{
    state={
        data: {a : 1 , b : 2 }
    }
    handerClick=() = >{
        const { data } = this.state
        data.a++
        this.setState({ data })
    }
    render(){
        const { data } = this.state
        return <div>
            <button onClick={ this.handerClick } >Click on the</button>
            <Text data={data} />
        </div>}}Copy the code

The effect

We clicked the button and found that
hadn’t been reupdated at all. This changes data but only the properties under data, so the PureComponent does not update for shallow comparisons.

It’s actually very easy to solve this problem.

<Text data={{ ... data }} />Copy the code

ShouldComponentUpdate () is called, regardless of whether the component is a PureComponent or not, and the result of its execution determines whether or not it is an update. If the component shouldComponentUpdate() is not defined, it will determine whether the component is a PureComponent. If it is, it will shallowEqual the props and states for the new component. If the new component is inconsistent, it will trigger a render update.

The react.memo function is similar to PureComponent. The react.memo function is used as the first higher-order component. The second argument is used to compare props to shouldComponentUpdate. Verify that props did not change, do not render the component, or render the component.

2 shouldComponentUpdate

Use shouldComponentUpdate() to let React know if a change in state or props affects the rerender of the component. Return true by default. Return false without rerendering the update. Also, this method is not called when the render is initialized or when forceUpdate() is used, as a shouldComponentUpdate application would normally do.

Control state

shouldComponentUpdate(nextProps, nextState) {
  /* Update the component when data1 changes */  
  returnnextState.data1 ! = =this.state.data1
}
Copy the code

This means that the component is updated only if data1 changes in state. Control prop properties

shouldComponentUpdate(nextProps, nextState) {
  /* When data2 changes in props, update the component */  
  returnnextProps.data2 ! = =this.props.data2
}
Copy the code

This means to update the component only if data2 changes in the props.

3 immetable.js

Immetable. js is a JAVASCRIPT library developed by Facebook that improves object comparison performance. As mentioned earlier, pureComponent is only a shallow comparison of objects. Immetable. js should be used with shouldComponentUpdate or React. Memo. In the immutable

Let’s use react-Redux as a simple example. The following data has been processed by immeTable.js.

import { is  } from 'immutable'
const GoodItems = connect(state= >
    ({ GoodItems: filter(state.getIn(['Items'.'payload'.'list']), state.getIn(['customItems'.'payload'.'list'])) || Immutable.List(), })
    /* Omit a lot of code here ~~~~~~ */
)(memo(({ Items, dispatch, setSeivceId }) = > {
   / * * /
}, (pre, next) = > is(pre.Items, next.Items)))

Copy the code

The is method is used to determine whether the Items(object data type) have changed before and after.

Five standard writing method, reasonable treatment of details

Sometimes, when we type code, we can avoid performance overhead by paying a little attention to the following. Perhaps just a few changes can be made to improve performance in other ways.

① Do not use arrow functions for binding events

Facing the problem

As you know, the react update is mostly due to props changes (passive rendering) and state changes (active rendering). When we bind an event to a child component that does not have any update qualification, or to a PureComponent that is pure, if we use the arrow function.

<ChildComponent handerClick={() = >{ console.log(Awesome!) }}  />
Copy the code

Each rendering creates a new event handler, which causes ChildComponent to be rendered every time.

Even if we use the arrow function to bind to the DOM element.

<div onClick={ () = >{ console.log(777) } } >hello,world</div>
Copy the code

Each time React synthesizes an event, it also redeclares a new event.

To solve the problem

To solve this problem, events are simple and can be divided into stateless components and stateful components.

Stateful component

class index extends React.Component{
    handerClick=() = >{
        console.log(Awesome!)
    }
    handerClick1=() = >{
        console.log(777)}render(){
        return <div>
            <ChildComponent handerClick={ this.handerClick} / >
            <div onClick={ this.handerClick1 }  >hello,world</div>
        </div>}}Copy the code

Stateless component

function index(){
   
    const handerClick1 = useMemo(() = >() = >{
       console.log(777)
    },[])  /* [] Existing dependencies on handerClick1 */
    const handerClick = useCallback(() = >{ console.log(Awesome!) },[])  /* [] Existing dependencies for handerClick */
    return <div>
        <ChildComponent handerClick={ handerClick} / >
        <div onClick={ handerClick1 }  >hello,world</div>
    </div>
}
Copy the code

For dom, if we need to pass parameters. We could write it this way.

function index(){
    const handerClick1 = useMemo(() = >(event) = >{
        const mes = event.currentTarget.dataset.mes
        console.log(mes) /* hello,world */}, [])return <div>
        <div  data-mes={ 'hello.world'}onClick={ handerClick1 }  >hello,world</div>
    </div>
}
Copy the code

(2) Loop the correct use of key

The correct use of key, whether react or vue, is to find the old node corresponding to the new node in a loop, reuse the node and save overhead. If you want to further understand, you can read another article of the author which comprehensively analyzes the VUe3.0 Diff algorithm, which has a detailed description of key. Let’s look at the correct and wrong ways to use key today.

1. Incorrect Usage

Error # 1: Using index as a key

function index(){
    const list = [ { id:1 , name:'ha ha'}, {id:2.name:'hey' } ,{ id:3 , name:'xi xi'}]return <div>
       <ul>
         {  list.map((item,index)=><li key={index} >{ item.name }</li>)}</ul>
    </div>
}
Copy the code

The performance of adding a key is similar to that of not adding a key, but diff from beginning to end.

Error # 2: Concatenating other fields with index

function index(){
    const list = [ { id:1 , name:'ha ha'}, {id:2.name:'hey' } ,{ id:3 , name:'xi xi'}]return <div>
       <ul>
         {  list.map((item,index)=><li key={index + item.name } >{ item.name }</li>)}</ul>
    </div>
}
Copy the code

If any elements are moved or deleted, then the one-to-one correspondence is lost and the remaining nodes are not effectively reused.

2. Correct Usage

Correct: Use a unique ID as the key

function index(){
    const list = [ { id:1 , name:'ha ha'}, {id:2.name:'hey' } ,{ id:3 , name:'xi xi'}]return <div>
       <ul>
         {  list.map((item,index)=><li key={ item.id } >{ item.name }</li>)}</ul>
    </div>
}
Copy the code

Using a unique key ID as a key enables efficient reuse of element nodes.

③ Stateless componentshooks-useMemoAvoid duplicating statements.

For stateless components, data updates are equivalent to repeated execution of the function context. So the variables in the function, the methods will be redeclared. Consider the following situation.

function Index(){
    const [ number , setNumber  ] = useState(0)
    const handerClick1 = () = >{
        /* Some operations */
    }
    const handerClick2 = () = >{
        /* Some operations */
    }
    const handerClick3 = () = >{
        /* Some operations */
    }
    return <div>
        <a onClick={ handerClick1 } >I have a surprise 1</a>
        <a onClick={ handerClick2 } >I have a surprise 2</a>
        <a onClick={ handerClick3 } >I have a surprise 3</a>
        <button onClick={() = >SetNumber (number+1)} > {number}</button>
    </div>
}
Copy the code

Every time you click on a button, the Index function is executed. HanderClick1 handerClick2, handerClick3 statement again. To avoid this, we can use useMemo for caching. We can change it to the following.

function Index(){
    const [ number , setNumber  ] = useState(0)
    const [ handerClick1 , handerClick2  ,handerClick3] = useMemo(() = >{
        const fn1 = () = >{
            /* Some operations */
        }
        const fn2 = () = >{
            /* Some operations */
        }
        const  fn3= () = >{
            /* Some operations */
        }
        return [fn1 , fn2 ,fn3]
    },[]) /* Redeclare the function only when the dependencies in the data change. * /
    return <div>
        <a onClick={ handerClick1 } >I have a surprise 1</a>
        <a onClick={ handerClick2 } >I have a surprise 2</a>
        <a onClick={ handerClick3 } >I have a surprise 3</a>
        <button onClick={() = >SetNumber (number+1)} > {number}</button>
    </div>
}
Copy the code

The following changes, handerClick1 handerClick2, handerClick3 will be cached.

Suspense and lazy load

Suspense and lazy can achieve dynamic import lazy loading in much the same way as routing lazy loading mentioned above. The way to use it in React is to use the

component in the Suspense component.

const LazyComponent = React.lazy(() = > import('./LazyComponent'));

function demo () {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>} ><LazyComponent />
      </Suspense>
    </div>)}Copy the code

LazyComponent is loaded through lazy Loading, so there can be delays in rendering pages, but with Suspense you can use

Loading… < div style = “box-sizing: border-box; color: RGB (255, 255, 255);

Suspense can package multiple lazy loaded components.

<Suspense fallback={<div>Loading...</div>} ><LazyComponent />
    <LazyComponent1 />
</Suspense>

Copy the code

Six more ways to avoid repeated renderings

Avoiding repetitive rendering is an important aspect of React performance optimization. If you want to do your best to handle every detail of the React project, start with each line of code and each component. Is the so-called not to accumulate silicon step without a thousand miles.

① Learn to use batch updates

Batch update

In this section, we will focus on the useState of stateless components and hooks, and this.setState of class stateful components. For example, here’s an example

In an update

class index extends React.Component{
    constructor(prop){
        super(prop)
        this.state = {
            a:1.b:2.c:3,
        }
    }
    handerClick=() = >{
        const { a,b,c } :any = this.state
        this.setState({ a:a+1 })
        this.setState({ b:b+1 })
        this.setState({ c:c+1 })
    }
    render= () = > <div onClick={this.handerClick} />
}
Copy the code

After the click event occurs, the setState is triggered three times, but it is not rendered three times because of the concept of batchUpdate. The final composition of the three setstates looks something like this

this.setState({
    a:a+1 ,
    b:b+1 ,
    c:c+1 
})
Copy the code

Stateless components

    const  [ a , setA ] = useState(1)
    const  [ b , setB ] = useState({})
    const  [ c , setC ] = useState(1)
    const handerClick = () = >{ setB( { ... b } ) setC( c+1 ) 
        setA( a+1)}Copy the code

Batch update failure

When we in view of the above two cases to be treated as follows.

handerClick=() = >{
    setTimeout(() = > {
        this.setState({ a:a+1 })
        this.setState({ b:b+1 })
        this.setState({ c:c+1})},0)}Copy the code
 const handerClick = () = > {
    Promise.resolve().then(() = >{ setB( { ... b } ) setC( c+1 ) 
    setA( a+1)})}Copy the code

We will find that in both cases, the component has been updated to render three times, at which point the batch update is invalid. This is also common in react-hooks, and it’s even more so in hooks, because we all know that in hooks, each useState keeps a state, instead of having a class declare component that can be harmonized with this. State, and again in asynchronous functions, For example, trying to change the state through multiple Usestates after an Ajax request causes the page to be rendered multiple times. To solve this problem, we can batch update manually.

Manual batch Update

React-dom provides the unstable_batchedUpdates method for manual batch updates. This API is more geared to react-hooks, which we can do.

 const handerClick = () = > {
    Promise.resolve().then(() = >{
        unstable_batchedUpdates(() = >{ setB( { ... b } ) setC( c+1 ) 
            setA( a+1)})})}Copy the code

So the three updates are merged into one. Also achieved the effect of batch update.

(2) merger state

Class component (stateful component)

Merging state, that’s kind of a habit that we’re going to develop in the React project. I’ve seen some of you do this in your code (the demo below is a simulation, it’s much more complicated than that).

class Index extends React.Component<any , any>{
    state = {
          loading:false /* To simulate loading effect */.list:[],
    }
    componentDidMount(){
        /* Simulate an asynchronous request data scenario */
        this.setState({ loading : true }) /* Enable loading effect */
        Promise.resolve().then(() = >{
            const list = [ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha'}, {id:3 , name: 'heihei'}]this.setState({ loading : false },() = >{
                this.setState({
                    list:list.map(item= >({
                        ...item,
                        name:item.name.toLocaleUpperCase()
                    }))
                })
            })
        })
    }
    render(){
    const { list } = this.state
    return <div>{
            list.map(item=><div key={item.id}  >{ item.name }</div>)}</div>}}Copy the code

Use this. State twice to remove the loading state and format the data list the second time. These other two updates are completely unnecessary and can be solved perfectly with a setState update. The reason for not doing this is that, while for simple structures like demo, a single update may be valuable for complex structures, we should learn to merge states. Modify the demo as follows.

this.setState({
    loading : false.list:list.map(item= >({
        ...item,
        name:item.name.toLocaleUpperCase()
    }))
})
Copy the code

Function components (stateless components)

For stateless components, we can store multiple states through a single useState; it is not necessary to use a single useState for each state.

For a case like this.

const [ a ,setA ] = useState(1)
const [ b ,setB ] = useState(2)
Copy the code

We can totally do it with a state.

const [ numberState , setNumberState ] = useState({ a:1 , b :2})
Copy the code

Note, however, that if our state is already a useEffect, useCallback, or useMemo dependency, use this method with caution.

③ useMemo React.memo isolation unit

React’s normal update stream, like a sword, passes through the parent component and child component. To avoid these repeated update renderings, the shouldComponentUpdate, React. Memo apis were created. But in some cases, unwanted updates are inevitable, such as this one. This update is passed down from the parent -> child component.

function ChildrenComponent(){
    console.log(2222)
    return <div>hello,world</div>
}
function Index (){
    const [ list  ] = useState([ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha'}, {id:3 , name: 'heihei'}])const [ number , setNumber ] = useState(0)
    return <div>
       <span>{ number }</span>
       <button onClick={() = >SetNumber (number + 1)} > Click</button>
           <ul>
               {
                list.map(item=>{
                    console.log(1111)
                    return <li key={ item.id }  >{ item.name }</li>})}</ul>
           <ChildrenComponent />
    </div>
}
Copy the code

The effect

We can address this phenomenon by using useMemo to isolate it and form a separate rendering unit, where the last state of each update is cached, the loop is no longer executed, and the sub-components are no longer rendered. We can do this.

function Index (){
    const [ list  ] = useState([ { id:1 , name: 'xixi' } ,{ id:2 , name: 'haha'}, {id:3 , name: 'heihei'}])const [ number , setNumber ] = useState(0)
    return <div>
       <span>{ number }</span>
       <button onClick={() = >SetNumber (number + 1)} > Click</button>
           <ul>
               {
                useMemo(()=>(list.map(item=>{
                    console.log(1111)
                    return <li key={ item.id }  >{ item.name }</li>
                })),[ list ])
               }
           </ul>
        { useMemo(()=> <ChildrenComponent />[])},</div>
}
Copy the code

Stateful component

There is no useMemo API for class-declared components, but this does not mean that we can block updates from the component itself using the React. Memo method. We can write a component that controls the direction in which the React component updates. We block the update flow with a

component.

/* Control updates, the second parameter can be used as a dependency for component updates, here set to ()=> true only render once */
const NotUpdate = React.memo(({ children }:any) = > typeof children === 'function' ? children() : children ,() = >true)

class Index extends React.Component<any.any>{
    constructor(prop){
        super(prop)
        this.state = { 
            list: [{id:1 , name: 'xixi' } ,{ id:2 , name: 'haha'}, {id:3 , name: 'heihei'}].number:0,
         }
    }
    handerClick = () = >{
        this.setState({ number:this.state.number + 1})}render(){
       const { list }:any = this.state
       return <div>
           <button onClick={ this.handerClick } >Click on the</button>
           <NotUpdate>{() = > (<ul>
                    {
                    list.map(item=>{
                        console.log(1111)
                        return <li key={ item.id }  >{ item.name }</li>})}</ul>)}
           </NotUpdate>
           <NotUpdate>
                <ChildrenComponent />
           </NotUpdate>
          
       </div>}}Copy the code
const NotUpdate = React.memo(({ children }:any) = > typeof children === 'function' ? children() : children ,() = >true)
Copy the code

Memo, which generates an isolation unit that blocks updates. If we want to control updates, we can start with the second argument of React. Memo, which completely blocks updates in the demo project.

④ ‘ban’ state and learn to use caches.

Instead of using state to manage data at all, they are good at using state, knowing when to use it and how to use it. React is not a responsive data stream like VUE. In vue, there is a special deP for dependency collection, which automatically collects the string template dependencies, as long as there is no reference data, this. Aaa = BBB, vue will not update the rendering. Because aaa’s DEP does not collect render watcher dependencies. In React, when we trigger this.setState or useState, we only care if the state value is the same twice to trigger the render, and we don’t care if the JSX syntax actually introduced the correct value.

There is no update state

Stateful components

class Demo extends React.Component{
    state={ text:111 }
    componentDidMount(){
        const { a } = this.props
         /* We just want to use text for props */ during initialization
        this.setState({
            text:a
        })    
    }
    render(){
        /* No text */ is introduced
       return <div>{'hello,world'}</div>}}Copy the code

In the example above, we didn’t introduce text into the render function. We just wanted to use text to record the value of the props a when we initialized it. Instead, we triggered a useless update with setState. The same applies to stateless components, as shown below.

Stateless components

function Demo ({ a }){
    const [text , setText] = useState(111)
    useEffect(() = >{
        setText(a)
    },[])
    return <div>
         {'hello,world'}
    </div>
}
Copy the code

To cache

Stateful components

In the class declaration component, we can bind data directly to this as a data cache.

class Demo extends React.Component{
    text = 111
    componentDidMount(){
        const { a } = this.props
        /* Save data directly on text */
        this.text = a
    }
    render(){
        /* No text */ is introduced
       return <div>{'hello,world'}</div>}}Copy the code

Stateless components

In stateless components, we can’t ask this, but we can use useRef to solve the problem.

function Demo ({ a }){
    const text = useRef(111)
    useEffect(() = >{
        text.current = a
    },[])
    return <div>
        {'hello,world'}
    </div>
}
Copy the code

(5) useCallback callback

The real purpose of useCallback is to cache an instance of the inline Callback each time it is rendered, so that it can be used with the shouldComponentUpdate or React. Memo of a child component to reduce unnecessary rendering. Child component rendering with limited source, child component props, but if the parent component of the callback, stateless components each rendering execution, will form a new callback, is unable to compare, so need to make a memoize memory in the callback function, We can understand useCallback as callback adding a memoize. Let’s move on to 👇👇👇.

function demo (){
    const [ number , setNumber ] = useState(0)
    return <div>  
        <DemoComponent  handerChange={() = >{ setNumber(number+1)  } } />
    </div>
}
Copy the code

Or the

function demo (){
    const [ number , setNumber ] = useState(0)
    const handerChange = () = >{
        setNumber(number+1)}return <div>  
        <DemoComponent  handerChange={ handerChange} / >
    </div>
}
Copy the code

Either way, the pureComponent and Reacte. memo are only able to determine that each update is a new callback, and then trigger the render update. UseCallback adds a memory function that tells our child components that the callback is the same twice without having to update the page. When the callback changes depends on the second parameter of the useCallback. Ok, so let’s rewrite that demo with useCallback.

function demo (){
    const [ number , setNumber ] = useState(0)
    const handerChange = useCallback( () = >{
        setNumber(number+1) 
    },[])
    return <div>  
        <DemoComponent  handerChange={ handerChange} / >
    </div>
}
Copy the code

This allows the pureComponent and React. memo to directly determine that the callback has not changed, preventing unnecessary rendering.

Use state management of standard rules

Whether we use Redux, dVA, Redux-Saga or MOBx derived from Redux, we must follow certain ‘rules of use’. The first thing that comes to my mind is when to use state management and how to apply state management properly. Next, let’s analyze it.

When to use state management

Ask me when state state management is appropriate. First of all, what is the problem that state management is designed to solve? The problems that state management can solve are mainly divided into two aspects. One is to solve the problem of cross-layer component communication. The second is to cache some global public state.

Our redux series of state management is an example.

I’ve seen it written by another student

Abuse of state management

/* and store below the text module list, establish dependencies, list update, component re-render */
@connect((store) = >({ list:store.text.list }))
class Text extends React.Component{
    constructor(prop){
        super(prop)
    }
    componentDidMount(){
        /* Initialize the request data */
        this.getList()
    }
    getList=() = >{
        const { dispatch } = this.props
        /* Get data */
        dispatch({ type:'text/getDataList'})}render(){
        const { list } = this.props
        return <div>
            {
                list.map(item=><div key={ item.id } >{/* do something to render the page.... * /}</div>)}<button onClick={() = >This.getlist ()} > Retrieve the list</button>
        </div>}}Copy the code

This page request data, data update, all in the current component, this writing METHOD I do not recommend, at this time the data go through the state management, eventually returned to the component itself, it is very weak, and did not play any role. The performance optimization is not as good as requesting data directly within the component.

Not using state management properly

Some of you might write it this way.

class Text extends React.Component{
    constructor(prop){
        super(prop)
        this.state={
            list: [].}}async componentDidMount(){
        const { data , code } = await getList()
        if(code === 200) {/* Obtain data that may be infrequent, multiple pages require data */
            this.setState({
                list:data
            })
        }
    }
    render(){
        const { list } = this.state
        return <div>{/* Drop-down box */}<select>
               {
                  list.map(item=><option key={ item.id } >{ item.name }</option>)}</select>
        </div>}}Copy the code

For unchanging data, data required by multiple pages or components, we can put the data in state management to avoid repeated requests.

How to use state management

Analysis of the structure

We need to learn to analyze pages, which data is constant, which is changing at any time, use the following demo page as an example:

As shown above, the red area is the data that is basically unchanged, and the data that may be needed by multiple pages can be put in the state management uniformly. The blue area is the data that is updated at any time, and it is good to directly request the interface.

conclusion

Constant data, multiple pages may need data, in state management, for frequently changing data, we can directly request the interface

Massive data optimization – time sharding, virtual list

Time slicing

The concept of time sharding is to render a large amount of data at once, and the initialization will appear lag and other phenomena. It is important to understand that js execution is always much faster than DOM rendering. Therefore, for a large amount of data, one-time rendering is easy to cause the situation of stalling and stalling. Let’s look at an example first

class Index extends React.Component<any.any>{
    state={
       list: []
    }
    handerClick=() = >{
       let starTime = new Date().getTime()
       this.setState({
           list: new Array(40000).fill(0)},() = >{
          const end =  new Date().getTime()
          console.log( (end - starTime ) / 1000 + '秒')})}render(){
        const { list } = this.state
        console.log(list)
        return <div>
            <button onClick={ this.handerClick } >Click on the</button>
            {
                list.map((item,index)=><li className="list"  key={index} >
                    { item  + '' + index } Item
                </li>)}</div>}}Copy the code

Let’s simulate rendering a list of 40,000 data at once and see how long it takes.

We saw 40,000 simple lists rendered in about 5 seconds. To solve the problem of loading large amounts of data at once. We introduced the concept of time sharding, which is to use setTimeout to divide tasks into several times to render. With 40,000 pieces of data in total, we can render 100 at a time and 400 at a time.

class Index extends React.Component<any.any>{
    state={
       list: []
    }
    handerClick=() = >{
       this.sliceTime(new Array(40000).fill(0), 0)
    }
    sliceTime=(list,times) = >{
        if(times === 400) return 
        setTimeout(() = > {
            const newList = list.slice( times , (times + 1) * 100 ) /* Capture 100 */ at a time
            this.setState({
                list: this.state.list.concat(newList)
            })
            this.sliceTime( list ,times + 1)},0)}render(){
        const { list } = this.state
        return <div>
            <button onClick={ this.handerClick } >Click on the</button>
            {
                list.map((item,index)=><li className="list"  key={index} >
                    { item  + '' + index } Item
                </li>)}</div>}}Copy the code

The effect

SetTimeout can use window. RequestAnimationFrame () instead of, can have a better rendering. Our demo uses the list to do, in fact, for the list, the best solution is virtual list, and time sharding, more suitable for the heat map, map more points.

Virtual list

I recently in the small program mall project, there is a long list of cases, but certainly said that the virtual list is the best solution to long list rendering. Whether it’s a small program or h5, as more and more DOM elements are added, the page will become more and more slow, and this is more obvious in small programs. In a later post, I will write a long list of rendering caching solutions for small programs. If you are interested, please follow me.

Virtual lists are an on-demand display technique that renders not all list items, but only a portion of the list elements within the visual area, based on the user’s scrolling. The normal virtual list is divided into render area, buffer, virtual list area.

See the figure below.

In order to prevent a large number of DOM presence from affecting performance, we only render the data in the render area and buffer, and there is no real DOM in the virtual list area. The buffer function is to prevent the rapid slide or slide up the process, there will be blank phenomenon.

react-tiny-virtual-list

React-tiny-virtual-list is a lightweight component that implements virtual lists. Here is the official document.

import React from 'react';
import {render} from 'react-dom';
import VirtualList from 'react-tiny-virtual-list';
 
const data = ['A'.'B'.'C'.'D'.'E'.'F'. ] ; render(<VirtualList
    width='100%'
    height={600}
    itemCount={data.length}
    itemSize={50} // Also supports variable heights (array or function getter)
    renderItem={({index, style}) = >
      <div key={index} style={style}> // The style property contains the item's absolute position
        Letter: {data[index]}, Row: #{index}
      </div>} / >.document.getElementById('root'));Copy the code

Write a react virtual list

let num  = 0
class Index extends React.Component<any.any>{
    state = {
        list: new Array(9999).fill(0).map(() = >{ 
            num++
            return num
        }),
        scorllBoxHeight: 500./* Container height (initialized height) */
        renderList: []./* Render list */
        itemHeight: 60./* Each list height */
        bufferCount: 8./* Buffer number of four */
        renderCount: 0./* Number of render */
        start: 0./* Start index */
        end: 0                /* Terminate index */
    }
    listBox: any = null
    scrollBox : any = null
    scrollContent:any = null
    componentDidMount() {
        const { itemHeight, bufferCount } = this.state
        /* Calculate the container height */
        const scorllBoxHeight = this.listBox.offsetHeight
        const renderCount = Math.ceil(scorllBoxHeight / itemHeight) + bufferCount
        const end = renderCount + 1
        this.setState({
            scorllBoxHeight,
            end,
            renderCount,
        })
    }
    /* Handle the scrolling effect */
    handerScroll=() = >{
        const { scrollTop } :any =  this.scrollBox
        const { itemHeight , renderCount } = this.state
        const currentOffset = scrollTop - (scrollTop % itemHeight)
        /* Translate3D turns on CSS CPU acceleration */
        this.scrollContent.style.transform = `translate3d(0, ${currentOffset}px, 0)`
        const start = Math.floor(scrollTop / itemHeight)
        const end = Math.floor(scrollTop / itemHeight + renderCount + 1)
        this.setState({
            start,
            end,
       })
    }
     /* Performance optimization: render the list only if the list start and end change */
    shouldComponentUpdate(_nextProps, _nextState){
        const { start , end } = _nextState
        returnstart ! = =this.state.start || end ! = =this.state.end 
    }
    /* Handle the scrolling effect */
    render() {
        console.log(1111)
        const { list, scorllBoxHeight, itemHeight ,start ,end } = this.state
        const renderList = list.slice(start,end)
        return <div className="list_box"
            ref={(node)= > this.listBox = node}
        >   
            <div  
               style={{ height: scorllBoxHeight.overflow: 'scroll', position: 'relative'}}ref={ (node) = >This.scrollbox = node} onScroll={this.handerScroll} > {/*<div style={{ height:` ${list.length * itemHeight}px`, position: 'absolute', left: 0.top: 0.right: 0}} / >{/* * * */}<div ref={(node)= > this.scrollContent = node} style={{ position: 'relative', left: 0, top: 0, right: 0 }} >
                    {
                        renderList.map((item, index) => (
                            <div className="list" key={index} >
                                {item + '' } Item
                            </div>))}</div>
            </div>

        </div>}}Copy the code

The effect

Specific ideas

Initialize the height of the compute container. Intercepts the length of the initialization list. Here we need the div to hold up the scrollbar.

(2) Monitor the onScroll event of the scroll container and calculate the upward offset of the render area based on the scrollTop. Note that when we slide down, in order to render the area in the visible area, the visible area must scroll upwards; When we slide up, the visible area should scroll down.

③ Re-render the list with recalculated end and start.

Performance optimization point

① To move the view area, we can use transform instead of changing the top value.

(2) Virtual list actual situation, is there a start or end change, in the re-render list, so we can use shouldComponentUpdate before tuning, to avoid repeated rendering.

conclusion

React performance optimization is a tough battle, which requires a lot of efforts to make our project more perfect. I hope people who have read this article can find the direction of React optimization and make our React project fly.

Feel useful friends can follow the author public number front-end Sharing to continue to update good articles.