React uses a lightweight, in-memory representation of the actual DOM called the virtual DOM to optimize UI updates. When a component’s state changes, React computes the diff between the virtual DOM and the real DOM, applying only the necessary changes.
While this process is fast, complex applications can still run into performance bottlenecks due to unnecessary component re-renders, heavy computations, or bloated bundle sizes. This guide outlines practical strategies and APIs you can use to optimize your React components.
Practical ways to optimize React components
1. Wrap functional components with React.memo
React.memo is a higher-order component that prevents functional components from re-rendering if their props have not changed. It is the functional component equivalent of React.PureComponent.
import { memo } from "react";
const MyComponent = ({ name }) => {
console.log("Rendering MyComponent");
return <h1>Hello, {name}!</h1>;
};
export default memo(MyComponent);
By default, React.memo performing a shallow comparison of props is sufficient. If you need fine-grained control, you can pass a custom comparison function as the second argument:
import { memo } from "react";
const MyComponent = ({ name, age }) => {
console.log("Rendering MyComponent");
return (
<div>
<h1>Hello, {name}!</h1>
<p>You are {age} years old.</p>
</div>
);
};
export default memo(MyComponent, (prevProps, nextProps) => {
// Only re-render if name or age actually changed
return prevProps.name === nextProps.name && prevProps.age === nextProps.age;
});
2. Lazy-load components with React.lazy and Suspense
You can reduce your application’s initial bundle size and speed up load times by deferring the loading of components that are not immediately visible on the screen.
import React, { lazy, Suspense } from "react";
const MyComponent = lazy(() => import("./MyComponent"));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
);
}
The Suspense container handles the loading state, rendering your fallback UI while the chunk containing MyComponent is fetched over the network.
3. Cache expensive calculations with useMemo
The useMemo hook lets you cache the result of a calculation between renders, recomputing it only when one of its dependencies changes.
import { useMemo } from "react";
function MyComponent({ data }) {
const filteredData = useMemo(() => {
return data.filter((item) => item.name.startsWith("A"));
}, [data]);
return (
<ul>
{filteredData.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
In this example, filtering the array only runs when the data prop changes, preventing expensive array operations on every render.
4. Cache functions with useCallback
When you pass a callback function to a memoized child component, React recreates that function on every render. This triggers a re-render in the child component despite prop memoization. Use useCallback to cache the function instance itself.
import { useCallback } from "react";
function MyComponent({ data }) {
const handleClick = useCallback(() => {
console.log(data);
}, [data]);
return <button onClick={handleClick}>Click me</button>;
}
5. Control effects with dependency arrays
Uncontrolled useEffect hooks that run on every render can cause performance lag or infinite rendering loops. Always declare an explicit dependency array to control when your side effects run.
import { useEffect, useState } from "react";
function MyComponent({ data }) {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`The data is ${data}`);
}, [data]); // Runs only when data changes
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
6. Implement shouldComponentUpdate in class components
If you are maintaining older class components, you can use the shouldComponentUpdate lifecycle method to manually determine whether prop or state changes warrant an update.
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return nextProps.name !== this.props.name;
}
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
7. Use PureComponent for class components
Alternatively, extend React.PureComponent instead of React.Component. Pure components implement a default shouldComponentUpdate with a shallow comparison of props and state.
class MyComponent extends React.PureComponent {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
8. Use React Fragments to avoid extra DOM nodes
Using unnecessary wrapper elements like div increases the size of your browser’s DOM tree, which can slow down page rendering. Use React Fragments to group elements instead.
function List({ items }) {
return (
<React.Fragment>
{items.map((item) => (
<React.Fragment key={item.id}>{item.text}</React.Fragment>
))}
</React.Fragment>
);
}
This groups your elements without inserting wrapper nodes into the final HTML output.
9. Be explicit instead of spreading props
Spreading props using the JSX spread operator (...) can make your code harder to read and optimize. It also runs the risk of passing invalid HTML attributes directly to DOM elements.
Instead of passing everything in an object:
// Avoid this
<MyComponent {...myProps} />
Explicitly define the props your component needs:
// Prefer this
<MyComponent id={myProps.id} className={myProps.className} />
This makes data dependencies clear and prevents unexpected prop updates from triggering unnecessary renders.
10. Virtualize long lists
Rendering thousands of list items at once can make your application sluggish and unresponsive. List virtualization speeds up rendering by drawing only the elements currently visible in the viewport.
Libraries like react-virtualized or react-tiny-virtual-list provide robust, pre-built components to handle list virtualization and smooth scrolling.
11. Divide code chunks with code-splitting
For large-scale applications, code-splitting allows you to split your final JavaScript bundle into smaller, page-specific chunks. These chunks are loaded dynamically on demand as the user navigates your site.
12. Profile performance with React Developer Tools
Use the profiling tab in the React Developer Tools browser extension to capture performance traces. The profiler shows exactly which components re-rendered, how long they took to render, and why the update occurred.