Tons of sites rely on the power of React, but how can you ensure your React Optimization is at its most effective? React is a popular JavaScript framework for building user interfaces for web, mobile, and desktop applications. In a recent StackOverFlow Developer Survey conducted in May 2024, React tops the list of the most popular front-end web frameworks used by developers.
While React is a popular choice for building high-performance web applications, it can become a significant performance bottleneck if not used properly. That’s why React Optimization is so essential–without it, you’ll deliver a poor, buggy, and slow experience that can drive users away from your site.
In this article, we’ll explore various techniques and strategies you can use to enhance the performance of your React applications. Ready to dive in? Let’s get started!
In this article:
Six Techniques for React Optimization
As a developer, it’s very easy to make unintended mistakes in the organization or structure of your React applications, leading to performance problems that may not be easily identifiable. Let’s take a look at some pitfalls you should avoid and React optimization techniques you can implement to make your applications more efficient.
#1. Avoid Mutating State
Mutating state directly in a React application can lead to performance issues, bugs, and unintended side effects. Props and states in React are immutable. When you mutate props and state directly, React may not recognize that the prop or state has changed, leading to inconsistent output and outdated UI.
The following examples from the React docs show how you should approach updating props and state in your apps:
When updating props, don’t do this:
function Post({ item }) { item.url = new Url(item.url, base); // 🔴 Bad: never mutate props directly return <Link url={item.url}>{item.title}</Link>; }
Do this instead:
function Post({ item }) { item.url = new Url(item.url, base); // 🔴 Bad: never mutate props directly return <Link url={item.url}>{item.title}</Link>; }
Likewise, when updating state, you should use the setter function returned by the useState
hook and avoid mutating the state directly.
Don’t do this:
function Counter() { const [count, setCount] = useState(0); function handleClick() { count = count + 1; // 🔴 Bad: never mutate state directly } return ( <button onClick={handleClick}> You pressed me {count} times </button> ); }
But do this instead:
function Counter() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); // ✅ Good: use the setter function returned by useState } return ( <button onClick={handleClick}> You pressed me {count} times </button> ); }
#2. Implement Code Splitting
Code splitting is a programming technique that breaks down a web application’s code into smaller chunks and loads them on demand, which can improve performance. Imagine a React app with multiple routes ( or pages). Technically, you might not need to show users all the components during the initial page load.
For example, a user visiting the homepage doesn’t need the code for the admin dashboard immediately. Code splitting allows you to load each route’s code only when the user visits that specific route.
React supports code splitting with dynamic import()
statements, which allows you to dynamically load modules at runtime, and React.lazy
, a React API used in conjunction with the Suspense
component to use lazy loading.
Here’s an example of dynamic import in React:
import("./math").then(math => { console.log(math.add(16, 26)); });
Below is another example of how to implement code splitting using React.lazy
and Suspense
:
import React, { Suspense } from 'react'; const LazyComponent = React.lazy(() => import('./LazyComponent')); function App() { return ( <div> <h1>Welcome to My App</h1> <Suspense fallback={<div>Loading...</div>}> <LazyComponent /> </Suspense> </div> ); } export default App;
#3. Use Memoization
Memoization is an optimization technique in computer programming that involves caching the result of an operation and returning the cached result when the same operation is performed again. In React optimization, memoization can be applied to components that are pure (a component that returns the same output if its props, state, and context are unchanged).
React components aren’t always pure in many scenarios because of state, which is essential in many dynamic web applications. To memoize a React component, we can wrap it in a memo
function as follows:
const MemoizedComponent = memo(MyComponent, arePropsEqual?)
Let’s take a look at the sample code below:
import React, { useState, memo } from 'react'; const MemoizedText = memo(({ text }) => { console.log(`Rendering MemoizedText component with text: "${text}"`); return ( <p>{text}</p> ); }, (prevProps, nextProps) => prevProps.text === nextProps.text); const ParentComponent = () => { const [count, setCount] = useState(0); const stableText = "This text never changes"; console.log("Re-rendering the parent component...") return ( <div> <MemoizedText text={stableText} /> <button onClick={() => setCount(count + 1)}> Increment({count}) </button> </div> ); }; export default ParentComponent;
When you run the code above, during the initial rendering, the two console.log()
statements “Re-rendering the parent component…” and “Rendering MemoizedText component with text: “This text never changes“, are printed in the terminal.
However, when we increment the counter, only the text “Re-rendering the parent component…” is printed to stdout. This is because when we click the “Increment” button, the ParentComponent
re-renders, but the MemoizedText
component does not re-render because its text
prop has not changed.
#4. Virtualize Long Lists
Virtualization, also known as windowing, is a React optimization technique that shows only a part of a large dataset, such as a long list or table, in the viewport of a screen. This reduces the time it takes to re-render components and the number of DOM nodes created.
react-window and react-virtualized are two commonly used libraries for virtualization in React. Here’s a simple example of windowing using the react-window
library to render a large list of items:
import { FixedSizeList as List } from 'react-window'; const Row = ({ index, style }) => ( <div style={style}>Row {index}</div> ); const Example = () => ( <List height={150} itemCount={1000} itemSize={35} width={300} > {Row} </List> );
#5. Lazy Load Images and Videos
Images, videos, fonts, and other digital assets are among the most common causes of performance concerns in web applications today. Large and uncompressed resources can have a significant impact on web app performance, particularly for users with low-end devices and poor internet connections.
Compressing and optimizing digital files and lazy-loading, caching, or sending assets via a CDN is critical for providing an enhanced user experience. Lazy loading and delivering content via a Content Delivery Network (CDN) through a media asset solution such as Cloudinary are effective strategies for improving app performance and user experience.
Apart from delivering content through a CDN, Cloudinary integrates well with React by providing additional optimization benefits such as optimizing images and videos quality on the fly and delivering assets in the correct format optimal for performance.
Here’s an example from the Cloudinary React SDK docs showing how to use the lazyload
and responsive
plugins to lazy load images:
import React from 'react' import {Cloudinary} from "@cloudinary/url-gen"; // Import plugins import {AdvancedImage, lazyload} from '@cloudinary/react'; const App = () => { // Create and configure your Cloudinary instance. const cld = new Cloudinary({ cloud: { cloudName: 'demo' } }); // Use the image with public ID, 'docs/colored_pencils'. const myImage = cld.image('docs/colored_pencils'); // Use the lazyload plugin return ( <div> <AdvancedImage cldImg={myImage} plugins={[lazyload()]}/> </div> ) };
#6. Cut Down the Size of Imported Packages
Performing application-wide audit, removing unused dependencies and dead code through tree shaking, minification, and so on, in a React project can be useful for keeping the project lightweight, reducing potential security risks, and improving performance.
For example, you can use a tool like depcheck to find unused dependencies in your project. The Import Cost extension in VS Code also displays the size of each imported package in your project, which can be useful in choosing lightweight alternatives.
Getting the Most From Your App with React Optimization
While React was developed to be efficient by default, it can still benefit greatly from a variety of optimization strategies for optimal performance. By leveraging strategies such as code splitting, lazy loading, memoization, windowing, and CDNs, etc., you can improve your application load speed, avoid wasteful re-renders, and maximize resource efficiency.
Luckily, with a solution like Cloudinary, you get to enjoy some of these optimization benefits automatically without writing complex code. Feel free to sign up for a free account and explore the Cloudinary React SDK documentation today to get started.