React Optimization
Murat Çiftci / April 01, 2023
10 min read •
Optimizing React App Performance using useCallback Hook and React.memo
Sometimes, when using React, we may experience performance issues on our pages due to page recompilation. In this article, we will see how we can solve these problems.
I will try to explain using an example application. I will go step by step, and I believe that writing together will help you understand better. So, let's start.
First, let's create a new React Vite project. Why am I using Vite instead of create-react-app? Because Vite offers a few features that are not available in create-react-app. One of them is that when you develop React applications with Vite, it only reloads the changing files instantly and preserves the state information without doing a full-reload. This way, you can have a faster development process. For more details, you can check the Vite documentation since it is not the main topic of our article.
npx create-vite my-app --template react
cd my-app
npm install
npm run dev
Our application will have a component that contains a simple username and password. It's not that simple in practice to design such a component without worrying about optimization issues. But in theory, we shouldn't worry about an optimization problem when designing such a component. However, things are not always that easy in practice.
import { useState } from 'react';
import './App.css';
function App() {
const [username, setUsername] = useState('');
return (
<div style={{ marginTop: '10px', padding: '5px' }}>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ padding: '5px', margin: '5px', outline: 'none' }}
/>
{username}
</div>
);
}
export default App;
In this component, whenever we click on the input, the state is updated, and the component re-renders to display the updated state on the screen.
Now, Let's create a UserComponent and pass the username state down as a prop to it:
function App() {
...
<UserComponent name={username} />
}
import { useLayoutEffect, useRef } from "react";
// Define a function to generate a random color
const getRandomColor = () => {
const randomNum = Math.floor(Math.random() * 16777215);
const hexColor = "#" + randomNum.toString(16);
return hexColor;
};
// Define the UserComponent function with a "name" parameter
const UserComponent = ({ name }) => {
// Create a reference to a div element using useRef()
const divRef = useRef(null);
// Use useLayoutEffect() to set the style of the div element
// whenever the "name" prop changes
useLayoutEffect(() => {
// Generate a random color using getRandomColor()
const color = getRandomColor();
// Set the color of the div element to the random color
divRef.current.style.color = color;
}, [name]);
// Return a div element with the "name" prop and inline styles
return (
<div ref={divRef} style={{ marginTop: "10px", padding: "5px" }}>
{name}
</div>
);
};
export default UserComponent;
The code creates a component called UserComponent that shows a name in a styled box. The color of the box changes to a random color whenever the name changes. This is done using the useLayoutEffect hook and a function that generates random colors. By using useLayoutEffect instead of useEffect, the color update happens before the browser paints the screen.
Now let's activate the 'Highlight when render' feature from the devtools.
-
Open your React application in the browser and open the React Developer Tools by right-clicking on a page, selecting "Inspect", and then selecting the "React" or "Components" tab in the developer tools.
-
Once you have opened the React Developer Tools, look for the "Highlight Updates" option at the top of the panel.
-
Check the box next to "Highlight Updates" to activate the feature. This will highlight the name of each component that updates or renders when you interact with your application.
-
You can now use this feature to optimize your React application by identifying components that are rendering unnecessarily and optimizing them using techniques like memoization or useCallback.
Now let's enter something into the input.
In React, whenever a component's state changes, React automatically re-renders the component and its child components in the component tree.
In our specific case, whenever the state variable "name" is updated in the App component, both the App and User components are re-rendered. The User component uses the "UseLayoutEffect" hook to assign a random color to a div element, and the "name" prop is passed as a dependency to this hook. As a result, every time the "name" prop changes, the hook is re-run and assigns a new random color to the div element.
Now let's spice things up and add a new component to our project. Let's make this component perform heavy operations.
function App() {
...
<UserComponent name={username} />
//Our newly added component that performs heavy operations.
<HeavyComponent />
}
import { useLayoutEffect, useRef } from "react";
const HeavyComponent = () => {
// useRef hook to keep track of whether the component is rendering for the first time or not
const isFirstRender = useRef(true);
// useRef hook to reference a div element in the component's JSX
const divRef = useRef(null);
// function to generate a random hex color
const getRandomColor = () => {
const randomNum = Math.floor(Math.random() * 16777215);
const hexColor = '#' + randomNum.toString(16);
return hexColor;
};
// useLayoutEffect hook to set the background color of the div element to a random hex color
useLayoutEffect(() => {
if (isFirstRender.current) {
// If the component is rendering for the first time, update the ref and exit
isFirstRender.current = false;
return;
} else {
// Otherwise, set the background color of the div to a random hex color and perform a heavy calculation
divRef.current.style.backgroundColor = getRandomColor();
for (let i = 0; i < 1000000000; i++) {}
}
});
return (
<div
style={{ marginTop: '10px', border: '1px solid black', padding: '5px' }}
ref={divRef}
>
Heavy Component
</div>
);
};
Now let's try entering a name again.
Why?
In React, when a parent component re-renders, it triggers a re-render of all of its child components, regardless of whether or not any of the child components' props have actually changed. This can lead to unnecessary re-renders and decreased performance, especially for heavy components that perform expensive calculations.
In our case, the HeavyComponent does not depend on any props passed down from the parent App component, and therefore does not need to re-render when the "name" state changes. To prevent unnecessary re-renders, we can use the React.memo higher-order component to memoize the HeavyComponent and only re-render it when its props have actually changed.
React.memo compares the props of the component between renders and only re-renders the component if the props have changed. This can significantly improve performance by preventing unnecessary re-renders of heavy components.
Therefore, by memoizing the HeavyComponent with React.memo, we can prevent it from re-rendering unnecessarily and improve the performance of our application.
const HeavyComponent = React.memo(() => {
...
})
React.memo creates a cached version of a component and compares previous and current props. If props haven't changed, React re-uses the cached version and avoids re-rendering.
By memoizing the HeavyComponent with React.memo, we have avoided unnecessary re-renders of the component and improved the performance of our application. This is because the HeavyComponent does not depend on any props, and therefore does not need to re-render unless there are changes to its internal state or props.
Now let's try passing an object and a function as props to our HeavyComponent:
function App() {
const [username, setUsername] = useState('');
const user = {
firstName: 'Murat',
lastName: 'Ciftci'
};
const handleClick = () => {
console.log('Button clicked');
};
...
<HeavyComponent user={username} handleClick={handleClick} />
}
So, even though we passed an object and a function as props to our HeavyComponent and thought that we solved the render issue by memoizing the component, we are now facing a performance issue again.
But why is that?
In JavaScript, objects are reference types, and whenever you create a new object, a new reference is also created in memory for that object.
So when you create the two object literals murat and , they are both new objects with their own unique references in memory, even though they have the same key name and the same value "murat".
React.memo only works with props that are primitive types or functions, because these types are compared by value, not by reference. If a prop is an object or an array, it will be compared by reference, and even if the contents of the object or array haven't changed, a change in reference will trigger a re-render.
So if you're using React.memo on a functional component that receives an object or an array as a prop, it may still re-render even if the contents of the prop haven't changed, because the reference to the prop has changed.
In this case, you can use techniques such as object or array destructuring, or pass down individual primitive props, to avoid passing down objects or arrays directly and use the useCallback hook for optimizing functions that are passed down as props.
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []);
Now, the handleClick function will only be re-created when its dependencies change, which in this case is an empty array [], indicating that the function doesn't depend on any props or state from the App component.
By using useCallback to memoize the handleClick function, you can prevent unnecessary re-renders of the Child component and improve the performance of your application.
<HeavyComponent name={user.name} surname={user.surname} handleClick={handleClick}/>
Now let's try entering a name again.
Blazingly Fast
Alright.. That's it..
Let's summarize..
In the previous example, by using React.memo to optimize a functional component, we were able to prevent unnecessary re-renders when the props of the component had not changed. However, React.memo only works with props that are primitive types or functions, since these types are compared by value and not by reference. When an object or an array is passed as a prop, it is compared by reference, and a change in reference will cause a re-render even if the contents of the prop have not changed.
To optimize functions that are passed down as props, the useCallback hook can be used. useCallback memoizes a function so that the same function reference is returned between renders as long as its dependencies have not changed. This can prevent unnecessary re-renders of child components that rely on the function as a prop.
Side Note:
It's important to use the useCallback function carefully and understand how it works. Applying useCallback to every function can cause unintended side effects, as it can result in more memory usage and slower performance due to the caching of unnecessary functions.
Therefore, it is recommended to only use useCallback when necessary and to pass down primitive props instead of objects or arrays to avoid unnecessary re-renders.
You can check out Kent C. Dodds' excellent article on this topic.
When to useMemo and useCallback
I hope I was able to help you with your performance problems. Thank you for reading, and happy optimizing