Site icon Primathon

What is Reconciliation, and How To Avoid it? | Primathon

What is Reconciliation and how to avoid? | Primathon

What is Reconciliation and how to avoid? | Primathon

There are many performance optimization concepts in react which helps us to build faster applications.

Today I am going to discuss one of most discussed concept in performance optimization: how to avoid Reconciliation

Table Of Content

What is Reconciliation?

  1. React maintains the internal representation of UI by creating a tree like structure of every DOM object in memory which is called Virtual DOM:

Basically it is just a javascript object which keeps information of every DOM object.

[Imagine virtual DOM as a javascript object which keeps information about every element]

{
    node: 'html',
    properties: {
        attributes: [],
        classes: [],
        events: []
    },
    children: {
        node: 'body'
        properties: {
            attributes: [],
            classes: [],
            events: []
        },
        children: [
            {        
                node: 'div',
                properties: {
                    attributes: [],
                    classes: [],
                    events: []
                },

            },
            {
                node: 'button',
                properties: {
                    attributes: [],
                    classes: [],
                    events: []
                }
            }
        ]
    }
}
  1. Whenever we update the state or props changes:

This whole process is how React updates DOM which is called Reconciliation.
This process is way faster than Real DOM manipulation.

Even though React is clever enough to update only changed nodes. But when props and state changes, re-rendering takes place which takes some time.

So we need to avoid unnecessary re-rendering for such cases.


The Case where we need to avoid Reconciliation or stop re-rendering process:

When parent component renders, all its child components are re-rendered even if their props or states didn’t change.
And if child component contains a very slow computation then, it will be computed in every re-render.

[Our Case Example-1 ]

The parent component contains:

In child component:

Even if you change the counter value using button element in Parent, the child component re-renders hence the computation along with it takes place which is quite noticable in example below.

Parent Component:

import React from 'react';
import Child from './Child';

class Parent extends React.Component {
  constructor() {
    super();
    this.state = {
      counter: 0,
      color: 'red'
    }
  }

  render() {
    console.log('[Parent] rendered');

    return (
      <div className="container">
          <div>Counter:  {this.state.counter}</div>

          <button onClick={() => this.setState({counter: this.state.counter + 1})}>Click me to change counter</button>

          <select defaultValue="red" onChange={(e) => this.setState({color: e.target.value})}>
            <option value="red">red</option>
            <option value="blue">blue</option>
            <option value="grey">grey</option>
          </select>

          <Child color={this.state.color} />
      </div>
    )
  }
}

export default Parent;

Child Component:

import React from 'react';

class Child extends React.Component {
    delay() {
        console.log('[Delay] function called');

        for(let i = 0; i < 5000000000; i++) {
            i++;
        }
        return 'delayed text';
    }

    render() {
        console.log('[Child] rendered');

        return (
            <div className="child">
                <hr />
                <div>{this.delay()}</div>
                Selected Color: {this.props.color}
            </div>
        )
    }
}

export default Child;

That’s where shouldComponentUpdate() comes in. It is the lifecycle method which is called before re-rendering starts in class based components.

1. should Component Update():

shouldComponentUpdate(nextProps, nextState)

[Example:]

/* Assume props = {color: 'red'} state={counter: 0} */

class Parent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
  if(this.nextProps.color === this.props.color) {
    return false;
  }
  if(this.nextState.counter === this.state.counter) {
    return false;
  }
  return true;
}

so if the color prop and counter variable in state doesn’t change then this component won’t re-render.

Optimizing performance of above case:

Example-1

Now we use shouldComponentUpdate() to compare the props and return false if props doesn’t changes.

So, now Child Component with shouldComponentUpdate:
import React from "react";

class Child extends React.Component {
  delay() {
    console.log("[Delay] function called");

    for (let i = 0; i < 800000000; i++) {
      i++;
    }
    return "delayed text";
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color === nextProps.color) {
      return false;
    } else {
      return true;
    }
  }

  render() {
    console.log("[Child] rendered");

    return (
      <div className="child">
        <hr />
        <div>{this.delay()}</div>
        Selected Color: {this.props.color}
      </div>
    );
  }
}

export default Child;

Live Examples on Codesandbox:

I have added the codesandbox urls below for both state of application before optimization and after optimization with shouldComponentUpdate().

In the example I have also added consoles so that we can observe which component are rendered and re-rendered.

  1. Without any performance optimization:

2. With shouldComponentUpdate():


2. React.PureComponent:

React also provides React.PureComponent which does shallow comparision of props and state to skip the re-rendering.

Using this we don’t have to write shouldComponentUpdate() manually, PureComponent internally manages it.

Example-1: our case when delay() function should be skipped if prop or state does not changes. We can achieve it using PureComponent.

import React from "react";

class Child extends React.PureComponent {
  delay() {
    console.log("[Delay] function called");

    for (let i = 0; i < 800000000; i++) {
      i++;
    }
    return "delayed text";
  }

  render() {
    console.log("[Child] rendered");

    return (
      <div className="child">
        <hr />
        <div>{this.delay()}</div>
        Selected Color: {this.props.color}
      </div>
    );
  }
}

export default Child;

It is much clearer code and does the work if shallow comparision works fine.


But it does not work if there is complex data structure or state is mutated in any way [Like exmaple below]:

Parent Component:

import React from "react";
import Child from "./Child";

class Parent extends React.Component {
  constructor() {
    super();
    this.state = {
      color: ["red"]
    };
  }

  render() {
    return (
      <div className="container">
        <select
          onChange={(e) => {
            console.log(this.state.color);
            let updatedColor = this.state.color;
            if (Array.isArray(this.state.color)) {
              updatedColor.push(e.target.value);
            }
            return this.setState({ color: updatedColor });
          }}
        >
          <option value="red">red</option>
          <option value="blue">blue</option>
          <option value="grey">grey</option>
        </select>

        <Child color={this.state.color} />
      </div>
    );
  }
}

export default Parent;

Child Component:

import React from "react";

class Child extends React.PureComponent {
  render() {
    return (
      <div className="child">
        <hr />
        Selected Color:
        <ul>
          {this.props.color.map((el, index) => {
            return <li key={index}>{el}</li>;
          })}
        </ul>
      </div>
    );
  }
}

export default Child;

Here child component doesn’t re-render even if you change the values in colors array.

Array is a reference type data struture in javascript and because PureComponent only does shallow comparision and both references are same each time.

To avoid this we can avoid mutation and send new array or objects each time.

Live: PureComponent example where state is mutated:


3. React.memo:

Example-1: our case when delay() function should be skipped if prop or state does not changes. We can achieve it using memo().

import React from "react";

function Child (props) {
    function delay() {
        for (let i = 0; i < 800000000; i++) {
            i++;
        }
        return "delayed text";
    }

    return (
    <div className="child">
        <hr />
        <div>{delay()}</div>
        Selected Color: {props.color}
    </div>
    );
}

export default React.memo(Child);

just wrap your component in React.memo() and it does shallow comparision on props.


Note by React docs: All the above methods only exists as a performance optimization. Do not rely on it to prevent a render, as this can lead to bugs

References:
React docs


Thanks for reading the article!

If you any questions, please comment here.

For more tech blogs be sure to follow us on twitter & linkedin.

Exit mobile version