Optimizing React Performance

Gülşah Vural
Papara Tech
Published in
8 min readAug 27, 2024

React is one of the most popular JavaScript libraries for building faster user interfaces. However, React applications can become slower and more complex as they grow. Recognizing and fixing potential issues is important to keep your app running fast.

In web development, performance is the key to providing a positive user experience. I’ll discuss some performance techniques in this article and share code examples.

Design is not just what it looks like and feels like. Design is how it works. — Steve Jobs

Memoization

. React.memo

React.memo is an effective way to avoid unnecessary re-rendering. This higher-order component (HOC) stores the rendered result of a component in memory. If there are no changes to the component’s props, the React.memo won’t re-render the component. This will help increase the performance of your react application. You can wrap your component into a React.memo, as shown below:

import { memo, useState } from 'react';

const MyApp = () => {
const [name, setName] = useState('');
const [address, setAddress] = useState('');

return (
<>
<label>
Name:
<input value={name} onChange={e => setName(e.target.value)} />
</label>
<label>
Address:
<input value={address} onChange={e => setAddress(e.target.value)} />
</label>
<Child name={name} />
</>
);
};

const Child = memo(({ name }) => {
console.log("child component is rendering");
return <h3>{name}</h3>;
});

export default MyApp;

In this example, you will see that the Child component re-renders when the name is changed, but not when address is changed. Normally, if the parent component re-renders, its child components do the same. But with memo, you can prevent a child component from re-rendering as long as its props haven't changed. This makes the component cached.

. useMemo

The useMemo hook stores the result of a function in memory and reuses it when the function's inputs haven’t changed. This is useful, especially for computationally expensive operations.

import React, { useMemo, useState } from 'react';

const ExpensiveCalculation = () => {
const [count, setCount] = useState(0);
const [inputValue, setInputValue] = useState('');


const calculation = (num) => {
console.log('Expensive Calculation');
for (let i = 0; i < 1000000000; i++) {
num += 1;
}
return num;
};

const calculatedValue = useMemo(() => {
return calculation(count);
}, [count]);

return (
<div>
<h1>Calculated Value: {calculatedValue}</h1>
<button onClick={() => setCount(count + 1)}>Increase Count</button>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Type something"
/>
</div>
);
};

export default ExpensiveCalculation;

In the example above, the calculation function takes a number and performs many operations on it, which makes it an expensive operation. By using useMemo, this expensive operation is only re-calculated when count changes. Even if inputValue changes, the calculation is not re-calculated because useMemo only depends on count.

. useCallback

useCallback is a hook that can be used to memorize callback functions. The callback functions will only be updated if one of the dependencies has changed. It helps prevent unnecessary re-renders of child components that use the callback as a prop.

import React, { useState, useCallback, memo } from 'react';

const ChildComponent = memo(({ onClick }) => {
console.log('ChildComponent rendered');
return <button onClick={onClick}>Click me</button>;
})

const ParentComponent = () => {
const [isDark, setIsDark] = useState(false);
const [count, setCount] = useState(0);

const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);

return (
<div>
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Dark Mode
</label>
<p>Count: {count}</p>
<ChildComponent onClick={handleClick} />
</div>
);
}

export default ParentComponent;

In the example above, if we don’t use useCallback, the ChildComponent would render whenever we changed the dark mode. It looks like:

React Fragments

With React Fragments, you don’t need to add extra nodes to the DOM. It allows you to group multiple elements.

This way, you avoid extra DOM elements from rendering.

return (
<React.Fragment>
<ChildA />
<ChildB />
</React.Fragment>
);

There is also a short syntax:

return (
<>
<ChildA />
<ChildB />
<>
);

Inline Functions

When you pass an inline function as a prop, React treats it as a new function each time the component re-renders. This can cause child components to re-render even if they haven’t changed. I will try to explain this with an example:

import React, { useState, useCallback } from "react"

const MyApp = () => {
const [isDark, setIsDark] = useState(false)
const callback = useCallback(() => {}, [])

return (
<>
<MyChild callback={() => callback()} />
<label>
<input type="checkbox"
checked={isDark}
onChange={(e) => setIsDark(e.target.checked)} />
Dark Mode
</label>
</>
)
}

export default MyApp

const Child = () => {
console.log("child component is rendering")
return <h1>Child Component</h1>
}

const MyChild = React.memo(Child)

In the example above, when I switch to dark mode, the Child component will re-render. Even if the callback function is memorized, the inline function passed as a property to the Child component is not memorized.

So what should we do?

import React, { useState, useCallback } from "react"

const MyApp = () => {
const [isDark, setIsDark] = useState(false)
const callback = useCallback(() => {}, [])

return (
<>
<MyChild callback={callback} />
<label>
<input type="checkbox"
checked={isDark}
onChange={(e) => setIsDark(e.target.checked)} />
Dark Mode
</label>
</>
)
}

export default MyApp

const Child = () => {
console.log("child component is rendering")
return <h1>Child Component</h1>
}

const MyChild = React.memo(Child)

We should use callback={callback}instead of callback={()=>callback()} .

The result is:

As you can see, the Child component is not re-rendered when I switch to the dark mode.

Key Prop in Lists

You need to pass each array item a key. When an array is rendered, the order of this array can be changed, deleted, or added. Key helps React to figure out which element in the array needs to be updated. However, there are some things to consider when creating keys. We should avoid using an index as a key. Instead, by defining unique keys, we can optimize React’s rendering process and improve performance.

export const people = [{
id: 0, // Used in JSX as a key
name: 'Creola Katherine Johnson',
}, {
id: 1, // Used in JSX as a key
name: 'Mario José Molina-Pasquel Henríquez',
}, {
id: 2, // Used in JSX as a key
name: 'Mohammad Abdus Salam',
}, {
id: 3, // Used in JSX as a key
name: 'Percy Lavon Julian',
}, {
id: 4, // Used in JSX as a key
name: 'Subrahmanyan Chandrasekhar',
}];

export default function List() {
const listItems = people.map(person =>
<li key={person.id}>
<b>{person.name}</b>
</li>
);
return <ul>{listItems}</ul>;
}

In this example, each element has a unique ID. Even if the order of the elements changes or elements are added or removed, there will be no issue. By identifying elements by their unique keys, React can efficiently update the DOM.

Code Splitting

Bundling is the method of combining imported files with a single file. Most React applications use tools like Webpack, Rollup, or Browserify for packaging. Bundling is great, but as your project grows, your bundle will grow too.

Code-splitting helps you to load only what the user needs at that moment. This way, we avoid unnecessary imports.

import React, { Suspense } from 'react';

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

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<Child />
<Child2 />
</section>
</Suspense>
</div>
);
}

Lazy Loading Images

When you use a lot of images in your React app, the performance of the app can be slow. This is because you are loading all the images that you have not shown to the user yet. With lazy loading, you can avoid rendering unless it is actually in the viewport.

react-lazyload and react-lazy-load-image-component are popular lazy-loading libraries that can be used in React projects.

import React from 'react';
import { LazyLoadImage } from 'react-lazy-load-image-component';

const MyImage = ({ image }) => (
<div>
<LazyLoadImage
alt={image.alt}
height={image.height}
src={image.src}
width={image.width} />
<span>{image.caption}</span>
</div>
);

export default MyImage;

Virtualizing / Windowing Long List Data

React virtualization processes only the data visible on the screen, instead of processing large amounts of data at once. It acts as a window, only rendering the components within that window. As you scroll, the content within the viewport changes.

The most popular npm packages are react-window and react-virtualized.

Debounce

Debounce is a technique that prevents a function from being called repeatedly within a given period. For example, let’s say a user typing text into a search box, and you want to send a request to a service using that text. Instead of sending the request immediately every time the user presses a key, you may want to send it after a certain time. This is very useful to reduce unnecessary operations and API requests in user interfaces.

There are two common ways to implement debounce in React: the first is to use lodash, a popular JavaScript library, and the second is to create your custom debounce hook. Here’s an example:

import { useEffect, useState } from 'react';

export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);

return () => {
clearTimeout(handler);
};
}, [value, delay]);

return debouncedValue;

This custom hook updates the value after the specified delay time. This way, we can perform the actions after the user stops typing and the specified time has passed.

There are many techniques to improve performance. I wanted to share some of these with you. These practices — React.memo, useMemo, useCallback, avoiding inline functions, code splitting, using react fragments, etc. — will help you improve performance, minimize unnecessary re-rendering, and provide a smoother, more efficient user experience.

Thanks for your time.

Reference:

--

--