Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

10 Tricks to optimize React app

Optimization is the number one thing that is on the mind of dev(s) when building software, especially web apps. JS frameworks like Angular, React, etc have included some awesome configurations and features to make them faster to appeal to users.

React has come to be the world number one JS framework. Being at the top means that it must be very fast. Overtime the brilliant React team has added features to React to make it highly performant. Here, I'll outline tricks that help make our React app very performant.

1. shouldComponentUpdate()

A React app is composed of components, from the root component usually App in App.js to the spreading branches.

class ReactComponent extends Component {
    render() {
        return (
            <div></div>
        )
    }
}

A basic React component.

These component trees make it have a parent-child relationship, that is when a bound-data is updated in a component, the component and its children are re-rendered to make the change propagate throughout the sub-component tree. When a component is to be re-rendered, React compares its previous data (props and context) to its current data (props and context), if they are the same there is no re-render, but if there is a difference the component and its children are re-rendered.

React compares the differences by object reference using the strict equality operator === since the props and context are objects. So React uses the reference to know when the previous props and state has changed from the current props and state.

class ReactComponent extends Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            data: null
        }
        this.inputValue = null
    }

    handleClick = () => {
        this.setState({data: this.inputValue})
    }

    onChange = (evt) => {
        this.inputValue = evt.target.value
    }

    render() {
        l("rendering App")
        return (
            <div>
                {this.state.data}
                <input onChange={onChange} />
                <button onClick={handleCick}>Click Me </button>
            </div>
        )
    }
}

See, the component above. It has data in the state object. If we enter a value in the input textbox and press the Click Me button, the value in the input will be rendered. I purposely added the l("rendering App") in the render method so we know when the ReactComponent renders.

Now, if we enter 2 and click the button the component will be rendered, It is supposed to be rendered because the previous state is this:

state = { data: null }

and the next state object is this:

state = { data: 2 }

because setState creates new state object every time it is called, the strict equality operator will see different memory references and trigger re-render on the component.

If we click on the button again, we will have another re-render, it shouldn't be so because the previous state object and the next state object will have the same data value, but because of setState new state object creation React will see diff state object references and trigger re-render despite them being of the same internal value.

Now, this re-rendering might be expensive if the component tree grows to thousands of components.

With this, React provides a way for us to control the re-rendering of our components instead of the internal logic by React, it is the shouldComponentUpdate method. This shouldComponentUpdate is called whenever the component is being re-rendered, if it returns true then the component is re-rendered, if false the re-rendering is canceled.

We will add the shouldComponentUpdate to the ReactComponent. This method accepts the next state object and next props object as an argument, so with this, we will implement our check to tell React when we wnt to re-render.

class ReactComponent extends Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            data: null
        }
        this.inputValue = null
    }

    handleClick = () => {
        this.setState({data: this.inputValue})
    }

    onChange = (evt) => {
        this.inputValue = evt.target.value
    }

    shouldComponentUpdate( nextProps,nextState) {
        if(nextState.data === this.state.data)
            return false
        return true
    }

    render() {
        l("rendering App")
        return (
            <div>
                {this.state.data}
                <input onChange={this.onChange} />
                <button onClick={this.handleClick}>Click Me </button>
            </div>
        )
    }
}

See, in the shouldCmponentUpdate, I checked the data value in the next state object nextState object and the current state object. If they not equal it returns true which will trigger a re-render, if they are not equal it returns false which cancels the re-render.

Run the app again, type 2 and continuously click the Click Me button, you will see that the rendering happens once and no more :)

See, we used the shouldComponentUpdate method to set when our component will be re-rendered effectively boosting the performance of our component.

2. useMemo()

This is a React hook that is used to cache functions in React, CPU-expensive functions.

Let's see an example:

function App() {
    const [count, setCount] = useState(0)
    
    const expFunc = (count)=> {
        waitSync(3000);
        return count * 90;
    }

    const resCount = expFunc(count)

    return (
        <>
            Count: {resCount}
            <input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
        </>
    )
}

We have an expensive function expFunc that takes 3 mins to execute, it takes an input count waits for 3 mins before returning the multiple of 90. We have a variable resCount that calls the expFunc with the count variable from the useState hook. We have an input that sets the count state whenever we type anything.

Whenever we type anything, our App component is re-rendered causing the expFunc function to be called. We will see that if we type continuously the function will be called causing a massive performance bottleneck. For each input, it will take 3 mins for it to be rendered. If we type 3, the expFunc will be run for 3 mins and if we type 3 again, it will take 3 mins again. It shouldn't run again in the second input because it's the same as the previous input, it should store the result somewhere and return it without running the function (expFunc).

Here, we will employ the useMemo hook to optimize the expFunc for us. useMemo has the structutre:

useMemo(()=> func, [input_dependency])

The func is the function we want to cache/memoize, the input_dependency is the array of inputs to the func that the useMemo will cache against, that is if they change the func will be called.

Now, using useMemo on our functional component App:

function App() {
    const [count, setCount] = useState(0)
    
    const expFunc = (count)=> {
        waitSync(3000);
        return count * 90;
    }

    const resCount = useMemo(()=> {
        return expFunc(count)
    }, [count])

    return (
        <>
            Count: {resCount}
            <input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
        </>
    )
}

Now, here the expFunc results will be cached against the input when the same input occurs again useMemo will skip calling the expFunc and return the output cached against the input.

This will make the App component highly optimized.

See, that useMemo caching technique to speed up performance. Also, it can be used to cache functional components against its props.

3. Virtualize long lists

If you render large lists of data, it is recommended that you render only a small portion of the datasets at a time within the visible viewport of a browser, then the next data are rendered as the lists is scrolled, this is called "windowing". Awesome React libraries have been built for this, there is the react-window and react-virtuaized by Brian Vaughn.

4. React.PureComponent

Just like what shouldComponentUpdate does to class components, so also React.PureComponent.

React.PureComponent is a base component class that checks the fields of state and props to know whether the component should be updated.

Let's convert our example in the shouldComponentUpdate section

class ReactComponent extends Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            data: null
        }
        this.inputValue = null
    }

    handleClick = () => {
        this.setState({data: this.inputValue})
    }

    onChange = (evt) => {
        this.inputValue = evt.target.value
    }

    shouldComponentUpdate( nextProps,nextState) {
        if(nextState.data === this.state.data)
            return false
        return true
    }

    render() {
        l("rendering App")
        return (
            <div>
                {this.state.data}
                <input onChange={this.onChange} />
                <button onClick={this.handleClick}>Click Me </button>
            </div>
        )
    }
}

to use React.PureComponent:

class ReactComponent extends React.PureComponent {
    constructor(props, context) {
        super(props, context)
        this.state = {
            data: null
        }
        this.inputValue = null
    }

    handleClick = () => {
        this.setState({data: this.inputValue})
    }

    onChange = (evt) => {
        this.inputValue = evt.target.value
    }

    render() {
        l("rendering App")
        return (
            <div>
                {this.state.data}
                <input onChange={this.onChange} />
                <button onClick={this.handleClick}>Click Me </button>
            </div>
        )
    }
}

See, we removed the shouldComponentUpdate and made the ReactComponent extend React.PureComponent.

Type 2 in the textbox and click on the Click Me button continuously, we will see that the ReactComponent will be re-rendered once and never.

It shallowly compares the fields of the previous props and state objects with the fields of the next props and state objects. It doesn't just do the object reference comparison of them.

React.PureComponent optimizes our components by reducing the number of wasted renders.

5. Caching functions

Functions can be called in the React component JSX in the render method.

function expensiveFunc(input) {
    ...
    return output
}

class ReactCompo extends Component {
    render() {
        return (
            <div>
                {expensiveFunc}
            </div>
        )
    }
}

If the functions get expensive, ie it takes long to execute it will hang the rest of the re-render code to finish thereby hampering the user's experience.

See, in the ReactCompo. The expensiveFunc is rendered in the JSX, for each re-rendered the function is called and the returned value is rendered on the DOM. The function is CPU-intensive, we will see that on every re-render, it will be called and React will have to wait for it to complete before running the rest of the re-rendering algorithm.

The best thing to do is to cache the function's input against the output so that the continuous execution of the function gets faster as the same inputs occur again.

function expensiveFunc(input) {
    ...
    return output
}

const memoizedExpensiveFunc = memoize(expensiveFunc)

class ReactCompo extends Component {
    render() {
        return (
            <div>
                {memoizedExpensiveFunc}
            </div>
        )
    }
}

6. reselect

Using reselect optimizes our Redux state management. As Redux practices immutability that means new object references will be created every time on action dispatch. This will hamper performance because re-render will be triggered on components even when the object references change but the fields haven't changed.

Reselect library encapsulates the Redux state and checks the fields of the state and tells React when to render or not if the fields haven't changed.

So, reselect saves precious time by shallowly traversing through the prev and current Redux states fields to check if they have changed despite having different memory references. If the fields have changed it tells React to re-render, if none fields have changed it cancels the re-render despite new state object created.

7. Web worker

JS code runs on a single thread. Running a long process on the same thread will seriously affect the UI-rendering code, so the best bet is to move the process to another thread. This is done by Web workers. They are the gateway where we can create a thread and run it parallel to the main thread without hampering the UI-flow.

We can use Web worker in React, though not officially supported, there are ways to add web worker to a React app. Let's see one:

// webWorker.js
const worker = (self) => {
    function generateBigArray() {
        let arr = []
        arr.length = 1000000
        for (let i = 0; i < arr.length; i++)
            arr[i] = i
        return arr
    }

    function sum(arr) {
        return arr.reduce((e, prev) => e + prev, 0)
    }

    function factorial(num) {
        if (num == 1)
            return 1
        return num * factorial(num - 1)
    }

    self.addEventListener("message", (evt) => {
        const num = evt.data
        const arr = generateBigArray()
        postMessage(sum(arr))
    })
}
export default worker

// App.js
import worker from "./webWorker"

import React, { Component } from 'react';
import './index.css';

class App extends Component {
    constructor() {
        super()
        this.state = {
            result: null
        }
    }
    calc = () => {
        this.webWorker.postMessage(null)
    }

    componentDidMount() {
        let code = worker.toString()
        code = code.substring(code.indexOf("{") + 1, code.lastIndexOf("}"))
        const bb = new Blob([code], { type: "application/javascript" });
        this.webWorker = new Worker(URL.createObjectURL(bb))
        this.webWorker.addEventListener("message", (evt) => {
            const data = evt.data
            this.setState({ result: data })
        })
    }

    render() {
        return ( 
            <div>
                <button onClick = { this.calc }> Sum </button>   
                <h3> Result: { this.state.result }</h3>  
            </div>
        )
    }
}

This app calculates the sum of the array with 1M elements, now if we had done this in the main thread, our main thread will hang till the 1M elements had been traversed and their sum computed.

Now, here we moved it to the Web worker, our main thread will run smoothly parallel to the web worker thread, while the sum of the 1M element-array will be computed. The result will be communicated when done and the main thread will only render the result. Quick, simple and highly performant.

8. Lazy loading

Lazy loading has come to be one of the optimization techniques widely used now to speed up the load time. The prospect of Lazy Loading helps reduce the risk of some of the web app performance problems to a minimal.

To lazy load route components in React, the React.lazy() API is used.

Using a little self-plagiarism:

React.lazy is a new feature added to React when Reactv16.6 was released, it offered an easy and straight-forward approach to lazy-loading and code-splitting our React components.

The React.lazy function lets u render a dynamic import as a regular component. - React blog

React.lazy makes it easy to create components and render them using dynamic imports. React.lazy takes a function as a parameter:

React.lazy(()=>{})

// or

function cb () {}
React.lazy(cb)

This callback function must load the component's file using he dynamic import() syntax:

// MyComponent.js
class MyComponent extends Component{
    render() {
        return <div>MyComponent</div>
    }
}

const MyComponent = React.lazy(()=>{import('./MyComponent.js')})
function AppComponent() {
    return <div><MyComponent /></div>
}
// or

function cb () {
    return import('./MyComponent.js')
}
const MyComponent = React.lazy(cb)
function AppComponent() {
    return <div><MyComponent /></div>
}

The callback function of the React.lazy returns a Promise via the import() call. The Promise resolves if the module loads successfully and rejects if there was an error in loading the module, due to network failure, wrong path resolution, no file found, etc.

When webpack walks through our code to compile and bundle, it creates a separate bundle when it hits the React.lazy() and import(). Our app will become like this:

react-app
 dist/
  - index.html
  - main.b1234.js (contains Appcomponent and bootstrap code)
  - mycomponent.bc4567.js (contains MyComponent)
/** index.html **/
<head>
    <div id="root"></div>
    <script src="main.b1234.js"></script>
</head>

Now, our app is now separated into multiple bundles. When AppComponent gets rendered the mycomponent.bc4567.js file is loaded and the containing MyComponent is displayed on the DOM.

9. React.memo()

Just like useMemo and React.PureComponent, React.memo() is used to memoize/cache functional components.

function My(props) {
    return (
        <div>
            {props.data}
        </div>
    )
}

function App() {
    const [state, setState] = useState(0)
    return (
        <>
            <button onClick={()=> setState(0)}>Click</button>
            <My data={state} />
        </>
    )
}

App renders My component passing the state to My via the data prop. Now, see the button sets the state to 0 when pressed. If the button pressed continuously, the state remains the same throughout but the My component would still be re-rendered despite the state passed to its prop being the same. This will be a huge performance bottle if there are thousands of components under App and My.

To reduce this, we will wrap the My component with React.memo which will return a memoized version of My, that will use in App.

function My(props) {
    return (
        <div>
            {props.data}
        </div>
    )
}

const MemoedMy = React.memo(My)

function App() {
    const [state, setState] = useState(0)
    return (
        <>
            <button onClick={()=> setState(0)}>Click</button>
            <MemeodMy data={state} />
        </>
    )
}

With this, pressing the button Click continuously will only trigger re-rendering in My once and never. This is because React.memo would memoize its props and would return the cached output without executing the My component so far as the same inputs occur over and over.

What React.PureComponent is to class components is what Reat.memo is to functional components.

10. useCallback()

From my previous article: Improve performance in functional components using useMemo, useCallback:

This works as useMemo but the difference is that it's used to memoize function declarations.

Let's say we have this:

function TestComp(props) {
    l('rendering TestComp')
    return (
        <>
            TestComp
            <button onClick={props.func}>Set Count in 'TestComp'</button>
        </>
    )
}

TestComp = React.memo(TestComp)

function App() {
    const [count, setCount] = useState(0)

    return (
        <>
            <button onClick={()=> setCount(count + 1)}>Set Count</button>
            <TestComp func={()=> setCount(count + 1)} />
        </>
    )
}

We have an App component that maintains a count state using useState, whenever we call the setCount function the App component will re-render. It renders a button and the TestComp component, if we click the Set Count button the App component will re-render along with its child tree. Now, TestComp is memoized using memo to avoid unnecessary re-renders. React.memo memoizes a component by comparing its current/next props with its prev props if they are the same it doesn't re-render the component. TestComp receives a prop actually a function in a func props attribute, whenever App is re-rendering, the props func of TestComp will be checked for sameness, if found being the same it will not be re-rendered.

The problem here is that TestComp receives a new instance of the function prop. How? Look at the JSX:

...
    return (
        <>
            ...
            <TestComp func={()=> setCount(count + 1)} />
        </>
    )
...

An arrow function declaration is passed, so whenever App is rendered a new function declaration is always created with a new reference(memory address pointer). So the shallow comparison of React.memo will record a difference and will give a go-ahead for re-rendering.

Now, how do we solve this problem? Should we move the function outside of the function scope, it will be good but it won't have reference to the setCount function. This is where useCallback comes in, we will pass the function-props to the useCallback and specify the dependency, the useCallback hook returns a memoized version of the function-prop that's what we will pass to TestComp.

function App() {
    const check = 90
    const [count, setCount] = useState(0)
    const clickHndlr = useCallback(()=> { setCount(check) }, [check]);

    return (
        <>
            <button onClick={()=> setCount(count + 1)}>Set Count</button>
            <TestComp func={clickHndlr} />
        </>
    )
}

Here, clickHndlr will not be re-created in every re-render of App component unless it dependency check changes, so when we repeatedly click on Set Count button TestComp will not re-rendered. useCallback will check the check variable if not same as its prev value it will return the function passed so TestComp and React.memo would see a new reference and re-render TestComp, if not same useCallback would return nothing so React.memo would see a function reference the same as its prev value and cancel the re-render of the TestComp.

Conclusion

React is awesome!!

The tricks we mentioned here must not be all implemented. Remember, don't optimize early, code the project first and then optimize where necessary.

The tricks were to show us ways to improve React app performance.

Thanks!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.