Optimize react apps and make it faster by aditya tyagi

Optimize Performance – React

Learn behind the scenes of React and make your apps blazing fast!

To get hold of any framework or library, one has to dive deep into it. Not only it helps to appreciate the abstraction one is getting but also helps to improve the debugging experience as a developer. The in-depth knowledge of a library will also help you to optimize the app.

There are certain things in React that frustrate newbies when they encounter and are left scratching their head! In this blog post, I’ll try to list down some of the things that have made me pull my hair out and knowing the underlying principles have brought me to peace!

Re-evaluating components ≠ Re-rendering the DOM

React as a library does not know there exists anything like “DOM”. It is the ReactDOM that communicates and interacts with the actual DOM. It is a library that only manages components and states. React then hands off all this information of what changed and what all should be visible on the screen to the interface i.e. ReactDOM it is working with. Basically React cares about

  1. the props (from the parent component)
  2. the state (internal data of the component)
  3. the context (the app-wide state)

Whenever the props, state or context changes, React checks if there is anything new to be rendered on to the screen and if it does have to render, it passes that information to the ReactDOM.

Virtual DOM

React uses something called Virtual DOM. Virtual DOM determines the component tree (which eventually gets rendered on the screen) and every component further has a sub-tree. So React determines the difference between the current virtual DOM and the new virtual DOM after any state changes and hands over this difference to the ReactDOM. The ReactDOM then receives the difference and manipulates the real DOM, if required.

So, whenever the state, context or props change, React re-evaluates and re-runs the component function.

Lets say we have a Demo component:

import React from "react";

const Demo = (props) => {
  return <p>{props.show ? "This is new" : ""}</p>;
};

export default Demo;

When the props on this component changes, the component function Demo will re-run and re-execute. On the other hand this does not mean that the entire real DOM will also re-render.

The real DOM will ONLY CHANGE in parts where the changes to props/state/context has affected the DOM. Thus, changes to real DOM are only made for differences between evaluations. Real DOM changes rarely and only when it is required.

This is a big performant advantage! Making a virtual comparison between previous and new state is comparatively cheaper because it only takes place in memory as compared to reaching out to the real DOM and then making comparisons. This concept of comparing the old and new state of virtual DOM is called Virtual DOM Diffing. This is another great writeup on the same.

Diffing algorithm: Behind the scenes

{/* consider this as a original state of the DOM */}
<div>
  <h1>Hello there</h1>
</div>


Now, imagine on a state change, a new <p> tag is introduced in the DOM. So, the ReactDOM will not render the entire thing again i.e. <div> and <h1>. But it will only render the difference.

<div>
  <h1>Hello there</h1>

  {/* Only this is rendered in the real DOM */}
  <p>This is a new para rendering on a condition</p>
</div>

To see it in action, I’ll render the above example in browser. I’ll also show you how it shows up in the developer tools because the difference will “Flash”.

Button toggles the visibility of <p> tag
Button toggles the visibility of <p> tag

For the above functionality, if you see closely in the developer tools, <p> flashes when it is added and removed and rest of the DOM remains intact. This proves that the real DOM only shows changes that are required and does not render the entire DOM unnecessarily.

DOM showing the change when <p> is toggled
DOM showing the change when <p> is toggled

The above example is valid even for components (with/without props) being rendered in JSX.

Consider re-evaluating of the component tied to state & prop changes. Whenever the state or the prop value changes for a component, that entire component is “re-evaluated”. This does not mean that the DOM i.e. the entire JSX returned by that component is also re-rendered. Only the parts where there is a difference in the virtual DOMs, the changes will take place in real DOM.

The re-evaluation includes re-initialisation of variables defined in the functional component as well as the functions created. But, React is smart enough to not re-initialise states and functions that are created using the React hooks like useState and useCallback. Thus, the setter functions and the state variables and initial states (for useState, useReducer), both are not re-initialised when the entire component is re-evaluated.

Let me take an example:

import React, { useState } from "react";
import { render } from "react-dom";

// Functional component
const DemoOutput = (props) => {
  return (
    <div>
      <h3>This is demo output component and will remain static</h3>
      <p>{props.show ? "Only this will render" : ""}</p>
    </div>
  );
};

// Main App component
const App = (props) => {
  const [showPara, setShowPara] = useState(false);
  const togglePara = () => {
    setShowPara((state) => !state);
  };
  return (
    <div>
      <h2>Virtual DOM demo by Aditya Tyagi</h2>
      <DemoOutput show={showPara} />
      <button onClick={togglePara}>Toggle</button>
    </div>
  );
};

render(<App messages={data.messages} />, document.getElementById("root"));

<p> tag flashing as this is the only state change. This is a way to optimize your JSX
<p> tag flashing as this is the only state change

As you can clearly see above that only <p> tag flashes when there is a state change to showPara variable and hence a change to prop value of show for the DemoOutput component.

This is a major performance incentive for React as the component sub-trees can be really complex and deeply-nested with the size of the apps.

Unnecessary Re-Evaluations

Whenever there is a state change in the parent component, the child component & the entire sub-tree of child components linked to it will also re-evaluate. That means, <DemoOutput> will re-evaluate every time there is a state change in the parent App component in the above example.

const App = (props) => {
  const [showPara, setShowPara] = useState(false);
  const togglePara = () => {
    setShowPara((state) => !state);
  };
  return (
    <div>
      <h2>Virtual DOM demo by Aditya Tyagi</h2>
      <DemoOutput show={showPara} />
      <button onClick={togglePara}>Toggle</button>
    </div>
  );
};

The re-evaluation of DemoOutput component makes sense when the props change. But what if the props never change.

<DemoOutput show={false} />

Here, the show prop will never change. Will the <DemoOutput> component still be re-evaluated? YES!

It sucks! I know but that is how React works!

This will not be an issue when the component is a small one. But the app will take a performance hit when the <DemoOutput> component is a huge component with its own multiple child components.

If there is async logic in DemoOutput or some memory intensive operation (heavy sorting), then it will make the app really slow! This is why we need to reduce the number of unnecessary re-evaluation of the components.

Consider this new DemoOutput component with a console statement.

import React from "react";

const DemoOutput = (props) => {
  console.log("Demo runs");
  return (
    <div>
      <h3>This is demo output component and will remain static</h3>
      <p>{props.show ? "Only this will render" : ""}</p>
    </div>
  );
};

export default DemoOutput;

In the example here, you’ll see that the console statement in DemoOutput component will run every-time there is a state change in the parent component even though the prop value of show never changes & is static to false.

Demo logs every-time there is change parent component but the prop for DemoOutput never changes. This app is not optimized
Demo logs every-time there is change parent component but the prop for DemoOutput never changes

React.memo( ) for Components in React

The problem above can be solved by using the memo() API on React. The memo comes in two forms:

  1. React.memo(Component)
  2. useMemo hook

This makes the use of “memoization” principle!

In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again.

– Google

To make a complete function memoized, we’ll wrap the function component in React.memo. This way the component will only render when there is an actual change to the prop value, thus reducing the number of unnecessary re-evaluations.

With <DemoOutput show={false} />, and wrapping the component while exporting in this way export default React.memo(DemoOutput); we have memoized the React component. Now, any state change in the parent component will NOT re-evaluate the DemoOutput component until the props are changing!

See how “Demo runs” do not log anymore in console.

Demo runs do not log anymore and is a step to optimize app
Demo runs do not log anymore

To test all these new things, please use the playground here:

React.memo() compares the props of the functional component with their previous state. Only if there is a change in any prop value of the component passed to memo() it will re-execute and re-evaluate the component.

Here, it will not re-execute the component repeatedly as the prop show passed to the component DemoOutput will never change from false. You can read more about React.memo() here.

This is the first step to avoid unnecessary re-execution of the entire react component.

The catch!

Try to use React.memo() with a prop value:

  1. Primitive – boolean, numbers, etc.
  2. Abstract – arrays, objects, functions

You will be surprised to know that even though the prop value never changes, the component is re-evaluated when the prop is an abstract type. For example, the component will re-execute, even with React.memo( ) for these examples:

<DemoOutput listItems={[1,2,3,4]} />

or

<DemoOutput onClick={onDemoClick} />

For primitive data types as prop data, it is comparing

props.show === previous.props.show.

For primitive this will work as false === false will give true because in JavaScript, comparing two primitive types work that way.

For abstract this will be different [1,2,3,4] === [1,2,3,4] will give false because comparing two abstract data types compare by reference and thus it gives false.

DemoOutput component will be re-rendered always as Function === Function will give false as all functions in JavaScript are objects. Objects again when compared by references gives false even though the object values are same!

Preventing Function Re-Creation using useCallback

When a parent component is re-executed and it has a child component with prop as function, the function being passed to it is always re-created. This has the side-effect of re-executing the child component function even though the function is same!

// in App.js (Parent component)
...
...
...

 // this function is re-created on ever re-execution of this function
  const onDemoClick = () => {
    console.log('Demo clicked!');
  }

  return (
    <div>
      <h2>Virtual DOM demo by Aditya Tyagi</h2>

       {/* because onDemoClick is re-created, the DemoOutput component is re-executed every-time, even though nothing changes in the function */}
      <DemoOutput onClick={onDemoClick} />

      <button onClick={togglePara}>Toggle</button>
    </div>
  );

So, to memoize objects (aka Functions), we can use useCallback hook. The function passed to this hook is memoized by React and on re-execution of the component, the function is not re-created.

The definition of useCallback is similar to that of useEffect. It takes in a dependency array too.

Why useCallback needs a dependency array?

useCallback needs a dependency array because of the “closures” phenomenon of functions in JavaScript. The functions close over the values available in their environment. Javascript locks the variables being used in the function when the function is defined for the very first time. Next time the function runs, the stored value of the locked variables will be used.

There are cases when we do need to re-create the function as the values being used by that function coming from outside might have changed. Hence the use of dependency array.

For example, if the function we are talking about here uses the values of a useState variable. There will be a requirement for feeding the function with new value of that variable once it has changed. If we omit the use of dependency array in useCallback, then on state change a re-execution of the function will take place, but our function will still have stale (old) value of that state variable.

// App.js (parent component)
...
...
...
  // this function is NOT re-created on ever re-execution of this function
  const onDemoClick = useCallback(() => {
    console.log("Demo click function invoked");
  }, []);

  return (
    <div>
      <h2>Virtual DOM demo by Aditya Tyagi</h2>

     {/* the function onDemoClick will not be re-recreated every-time on re-exection of this parent. It will only be re-created when any of the params passed to dependency array changes. */}
      <DemoOutput onClick={onDemoClick} show={false} />

    </div>
  );

This only works when the DemoOutput component is wrapped in React.memo(DemoOutput).

Using useMemo() hook

Consider this example wherein we are sorting the items array coming from parent component in DemoOutput component.

// DemoOutput.js
import React from "react";

const DemoOutput = (props) => {

  // a sorting function that has some complex sorting algorithm
const sortedItems = props.items.sort((a,b) => {
  // some elaborate sorting algorithm
  return a-b;
})


  return (
    <div>
      <h3>{props.title}</h3>
      <ul>
        {sortedItems.map(item => {
          return <li>{item}</li>
        })}
        </ul>
    </div>
  );
};

export default React.memo(DemoOutput);

The above component sorts the incoming items on the prop items and then displays the items.

Now, consider we change just the title on props. You’ll see the DemoOutput component re-executes. To be honest, it should and that’s fine. What’s concerning here is that the entire sorting algorithm runs again every time, even though the input (props.items) never changed. This is again a performance hit and can make your app slow for no reason at all!

To tackle this, let me introduce you to the useMemo hook.

Using the useMemo hook, you can memoize the sorting functions and other objects! Thus changing the sorting function to use the hook, it will only run when the params passed to its dependency array changes.

  // a sorting function that has some complex sorting algorithm
const sortedItems = useMemo(() => {
  return items.sort((a,b) => {
    // some elaborate sorting algorithm
    return a-b;
  });
}, [items])

This will reduce the stress on the browser and thus will add to performance of the app!

Drawbacks of memoization

This optimisation comes at an additional cost. When we put a component into memo, React has to store the previous values of the props so that it has something to compare with. This takes additional time and memory. Therefore, we do not add memo to every component.

The trade-off depends on the level of the component tree you are trying to optimise. For a long & complex component tree branch, using memo makes sense.

Components and State

Everything comes down to state/props/context which controls the re-rendering of the screen.

Every-time the function component re-executes due to state change, we don’t see the re-initialisation of useState , useReducer & other hooks React provides.

This is because useState comes from React and it does that management for you.

For example:

const [isLoading, setIsLoading] = useState(false);

Here,

isLoading – state variable

setIsLoading – setter for the state variable

false – initial value

So React ensures that the useState and the initial values passed is considered only once – the very first time the component runs. React then remembers to which component that initial values belongs.

BUT, It will re-initialise the useState if the component was completely removed from the DOM. It can either be conditionally or the user exiting from that component/page.

Understand State Updates and Scheduling to optimize performance

Consider the same example we had above

const [isLoading, setIsLoading] = useState(false);

The state does not immediately updates when the setter is called. It is scheduled for later. The order is always kept for state changes in the schedule.

If we again call the state setter to update the state before the 1st state change happened, the 2nd state change will NOT precede the 1st change. The order remains intact.

Let me explain with an example:

Consider this piece of code in App.js. There are two buttons which are tied to two functions respectively.

// App.js
...
...
...
const [isLoading, setIsLoading] = useState(false);

// Button 1  
const updateLoading = () => {
    setIsLoading(true);
    setIsLoading(false);

    // setting isLoading to "true" at last
    setIsLoading(true);

    // it will still print "false"
    console.log(isLoading);
  }


// Button 2
  const checkLoading = () => {
    // will print "true"
    console.log(isLoading);
  }

  return (
    <div>
      <button onClick={updateLoading}>Set loading</button>
      <button onClick={checkLoading}>Check loading</button>
    </div>
  );

When updateLoading is clicked, even though the last setIsLoading sets the state to true, we will get false consoled. The state change done by this function is SCHEDULED for later. It will come into play when there is some delay/async tasks performed or a re-render of the function happens.

Now, when checkLoading button is clicked (after a delay), it will print true as the SCHEDULED value is true.

This is the scheduling principle that we need to understand and grab because this is the complicated piece that most of the developers overlook or mis-understand while learning React. This is also a principle one has to keep in mind while debugging React apps.

– NOTE TO SELF

If there are a a number of state changes calls, we’ll get the result from the last execution of the function component. All state changes will not take the intermediate state changes values into account and only give back the result from the very last function execution.

That is the reason to use the function form for updating the state if you depend on the previous state snapshot.

setIsLoading((prevState) => !prevState);

Hence Function form – for every state change, it looks into the outstanding state and gives you that. It DOES NOT use the state from the last execution of the component.

NOTE: There is a difference between when the component was re-rendered and when the state change was scheduled. You can have multiple state changes from a single function execution.

If you have 2 state update back to back in a single piece of block, without any callbacks/promises/time-delays in between it will not result in 2 re-execution of the functional component. It will batch the two in 1 re-execution.

// Button 1  
const updateLoading = () => {
    setIsLoading(true);
    // ...
    // state is not updated here
    // ...
    setIsLoading(false);
    // ...
    // state is not updated here
    // ...
    setIsLoading(true);

    // it will still print "false"
    console.log(isLoading);
  }

All three state changes will be combined into 1 function re-execution


Keeping the above pieces in mind, not only you’ll be able to optimise your React applications but will be able to have a better debugging experience too! I hope you find this helpful and interesting. In case any questions appear I would be glad to answer them!

Leave a Reply

Your email address will not be published. Required fields are marked *