React Optimisation ⚙️

React Optimisation ⚙️

In this blog post, you will be introduced to performance issues and how to optimise your react code.

·

6 min read

1. Introduction

We know that react makes changes to only necessary DOM elements that have changed in an execution cycle due to changes in state in any of the components. It doesn’t re-render the whole DOM whenever a state change occurs in a component. We also know that change in state causes the entire component to re-render.

However, if that component contains any child elements, they will be re-rendered too. Yes!, that’s how React works! That’s the default behaviour of React. Now imagine, change in state inside a component doesn’t need its child components to re-render. It can be due to one of the reasons that the new state doesn’t effect any change in its children (props passed from the parent to its children data doesn’t change or a child component might not use any state changed data from the parent). In that case, re-rendering such child components again might be a performance bottleneck.

If we are sure that any child component need not re-render due to a state change in its parent component then we might however can prevent it.

using React.memo():

The React.memo() prevents a child component to re-render if not needed. React.memo() compares the data from the previous execution cycle to the current executing cycle. If there is no change in data from the previous execution cycle to current execution cycle, then React.memo() prevents the execution of this component. However, if there is, then React.memo() is powerless and executes.

Consider the following example,

/*Child component that doesn't receive any data from parent but is a part of it*/
import React from 'react';

const Button = () => {
    <button>Click me!</button>
);
/* Using React.memo() method at this level in a child component */
export default React.memo(Button);

Note: By using React.memo() we exchange the power of complete re-execution of component to comparing the values of the last and current execution cycles. However, performance costs whilst comparing state values as well.

If we use React.memo() unintentionally at a place where the component is gonna re-render at any cost, then we are not only affording performance cost to causing entire component to re-render but also to comparing values of last and current execution cycle.

Ok!, so that is about how you can prevent unnecessary execution overhead. Now, that's not only it. There's something else I would want to introduce you to.

2. Performance cost due to default Javascript execution behaviour

As we previously discussed, change in state of a component causes entire component to re-render. However, this has something to do with JavaScript behaviour as well.

As I mentioned that React.memo() compares values to decide whether or not the component has to re-render. This comparison varies to comparing boolean values and comparing reference types.

For better understanding, let me split this into two scenarios

  • Case-1: execution behaviour with primitive types

  • Case-2: execution behaviour with reference types

Case-1: No matter during any no.of execution cycles, primitive(boolean, string, etc..) values are always built-in JS types. Hence, at any execution instance, if their value doesn’t change, then that component is not re-rendered.

Case-2: In the case of reference types (objects, etc…) at every execution cycle instance, a new memory location is allocated and hence when comparing to previous execution cycle to the current execution cycle, the reference to memory location changes even though the value may be the same. Hence, in this case, though the value doesn’t seem to change the reference changes at every execution cycle and hence causing the whole component to re-execute.

Case-1: No re-render

const Parent = () => {
    const [val, setVal] = useState(false);
    /* the value is hardcoded and doesn't use the val i.e value attribute doesn't
            depend on the val variable */
    /* also the fact that it is a primitive type resulting in prevention of 
            re-rendering the child component */
    return <Child value={true} />;

};

export default Parent;
/* In this case, the component doesn't re-render */
import React from 'react';

const Child = (props) => {
    return <p>{props.value}</p>;
};

/* This helps */
export default React.memo(Child);

Case-2: re-render:

  • Here we might assume that the function onClickHandler() remains same at every execution cycle and hence shouldn’t cause entire component to be re-rendered.

  • However, this is not the case. When state is changed in the Parent component, the entire component gets re-executed by react and the on onClickHandler() function is created newly at every execution cycle causing it to change the reference, resulting in re-rendering of its child component as well as the function it uses.

const Parent = () => {
    const [val, setVal] = useState(false);
    const onClickHandler = () => {
        /* Do something here ... */
    };
    /* A new function object is created at every execution cycle causing the 
            comparison by React.memo() to fail due to different references at different
            execution cycles. */
    return <button onClickDo={onClickHandler}>Click me!</button>;
};

export default Parent;
/* This component re-renders */
import React from 'react';

const Child = (props) => {
    /* Do something here ... */
    if(/*some condition*/ ? props.onClickDo : /*something else */)
};

/* This doesn't help any ways and causes extra overhead */
export default React.memo(Child);

Any way around?

At this point of time, if you are wondering "What can we about this?", "Is there a way around?". Well, I got you covered! ✌️. Here's something we can do about this.

useCallback() hook:

  • React provides a useCallback() hook that takes in a function as first param and second param as a set of dependencies (similar to useEffect hook).

  • Dependency array is needed because any state change causing dependencies used inside the function to change should enable the whole function to be re-created and stored in the memory.

  • If not done so (having dependency array empty), the outer state variables used inside the callback are not tracked and the function never gets created or updated after the first execution and this is not always what we want.

  • We may want to track those variables (with updated values) and use them in our function and still lock it in the memory. This is where dependency array helps solve the issue.

import { useCallback } from 'react';

const Parent = () => {
    const [val, setVal] = useState(false);
    const onClickHandler =     /* this is new */ useCallback(() => { 
        /* Do something here ... */
    }, [val]);

    return <button onClickDo={onClickHandler}>Click me!</button>;
};

export default Parent;

That's great knowing this so far. However, it would still be incomplete to stop right here without knowing about useMemo() hook.

useMemo() hook:

  • It can be used to store objects or intensive tasks that we might not want to re-execute whenever the component re-renders as long as that data doesn’t change.

  • If the data does change, then we might want to add it as a dependency to the useMemo() hook.

      import { useMemo } from 'react';
    
      const App = () => {
      /* also an added advantage that this array is stored and locked once as long as the values of the arrays doesn't change. As we previously discussed that array is ref type and hence ... */
          const arr = useMemo(() => [1,6,3,5,2], []);
          const sortedArr = useMemo(() => arr.sort((a, b) => a - b), [arr]);
      };
    
      export default App;
    

    3. Conclusion

    In conclusion, optimizing React applications is essential to ensure a smooth user experience and efficient performance. The techniques discussed in this blog post, such as using React.memo(), useCallback(), and useMemo() hooks can greatly improve the performance of your React apps. There are lot of other ways you can do to optimize your application depending upon your application requirements. Implementing the techniques outlined in this post is a great starting point for optimizing your React applications. Happy optimizing! 😉