How to optimise React application: Best practices
React is one of the most popular front-end JavaScript libraries in the world. Moreover, employers are increasingly choosing React, so the better you know the framework, the easier it is for you to get a job.
I have been working with the framework for many years and in that time I have learned what rules to follow, because as your React application grows in size and complexity, it may start to slow down, impacting the user experience. To prevent this, you need to optimise your React application.
Optimising your React application involves improving its performance, reducing its load time, and making it more efficient. In this article, we will discuss some best practices and tips on how to optimise React application.
Components simple and focused:
The first step in optimising a React application is to keep your components simple and focused. When a component does too many things, it becomes difficult to maintain and can cause performance issues. To keep your components simple and focused, you should:
- Split your components into smaller, more focused components
- Use the Single Responsibility Principle (SRP) to ensure each component does one thing well
- Avoid using the shouldComponentUpdate lifecycle method unless necessary
import React from "react";
// This component is responsible for rendering a list of items.
function ItemList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// This component is responsible for fetching and managing the items.
function ItemListContainer() {
const [items, setItems] = React.useState([]);
React.useEffect(() => {
// Fetch items from an API endpoint
fetch("/api/items")
.then((response) => response.json())
.then((data) => setItems(data))
.catch((error) => console.error(error));
}, []);
return <ItemList items={items} />;
}
export default ItemListContainer;
By separating these responsibilities into two separate components, we ensure that each component has a single responsibility and is focused on doing one thing well. This makes our code easier to understand, test, and maintain.
Use Pure components:
Pure components are components that do not depend on external state or props. They only render based on their internal state, which makes them faster and more efficient. To use pure components, you should:
- Use functional components instead of class components
- Use the React.memo() higher-order component to memoize your components
import React from "react";
// This component renders a list of items.
function ItemList({ items }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
// This is a memoized version of the ItemList component.
// It only re-renders if the items prop has changed.
const MemoizedItemList = React.memo(ItemList);
// This component fetches and renders the list of items.
function ItemListContainer() {
const [items, setItems] = React.useState([]);
React.useEffect(() => {
// Fetch items from an API endpoint
fetch("/api/items")
.then((response) => response.json())
.then((data) => setItems(data))
.catch((error) => console.error(error));
}, []);
return <MemoizedItemList items={items} />;
}
export default ItemListContainer;
Memoization:
Memoization is a technique used to store the results of expensive computations so that they can be reused later. Memoization can significantly improve the performance of your React application by reducing the number of expensive computations. To use memoization, you should:
- Use the useMemo() hook to memoize expensive computations
- Use the useCallback() hook to memoize functions
import React from "react";
// This component takes a list of items and returns the total price.
function calculateTotalPrice(items) {
console.log("Calculating total price...");
return items.reduce((total, item) => total + item.price, 0);
}
function ShoppingCart({ items }) {
const totalPrice = React.useMemo(() => calculateTotalPrice(items), [items]);
const handleItemClick = React.useCallback((item) => {
console.log(`Clicked item ${item.name}`);
}, []);
return (
<div>
<h2>Shopping Cart</h2>
<ul>
{items.map((item) => (
<li key={item.id} onClick={() => handleItemClick(item)}>
{item.name} - ${item.price}
</li>
))}
</ul>
<p>Total Price: ${totalPrice}</p>
</div>
);
}
export default ShoppingCart;
To handle clicks on each item, we define a handleItemClick function using the useCallback hook. This function is memoized so that it doesn't get recreated every time the component re-renders.
Props drilling:
This occurs when you have to pass data down through multiple layers of components to reach a child component that needs it.
For example, imagine you have a top-level component that fetches data from an API and passes it down to its child components. One of those child components may then need to pass that data down to one of its own child components. This can create a chain of "prop drilling" that can become unwieldy and difficult to manage.
To avoid props drilling, there are several techniques you can use:
Context: You can create a context object at a higher level and then consume it in any child component that needs the data.
Redux: With Redux, you can create a global store that contains all the data your components need, and then access that data from any component without having to pass it down explicitly.
Higher-order components (HOCs): You can use HOCs to pass down props that multiple components need, without having to drill down through every component.
Render Props: Render props are a technique where a component's children are a function that receives the necessary data as an argument. This allows you to pass down data to a child component without having to drill down through every component.
Avoid inline functions:
Inline functions can cause unnecessary re-renders and impact the performance of your application. To avoid inline functions, you should:
- Define your functions outside of the render method
- Use the useCallback() hook to memoize your functions
import React from "react";
function MyComponent() {
const [count, setCount] = React.useState(0);
// This function is defined outside of the render method.
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h2>My Component</h2>
<p>Count: {count}</p>
<button onClick={handleClick}>Click Me</button>
</div>
);
}
export default MyComponent;
Instead of defining the handleClick function inline in the render method of the component, we define it outside of the render method. This is because defining functions inline can cause unnecessary re-renders of our component, which can lead to performance issues.
Avoid object creation in render methods:
Creating new objects in the render method can cause memory leaks and impact the performance of your application. To avoid object creation in render methods, you should:
- Define your objects outside of the render method
- Use the React.memo() higher-order component to memoize your components
import React from "react";
function MyComponent({ user }) {
// We define the profile object outside of the render method.
const profile = React.useMemo(() => {
return {
name: user.name,
age: user.age,
email: user.email,
address: user.address,
};
}, [user]);
return (
<div>
<h2>My Component</h2>
<p>Name: {profile.name}</p>
<p>Age: {profile.age}</p>
<p>Email: {profile.email}</p>
<p>Address: {profile.address}</p>
</div>
);
}
export default MyComponent;
Code splitting:
Code Splitting is a technique used to split your code into smaller chunks that can be loaded on-demand. This improves the performance of your application by reducing the initial load time. To use code splitting, you should:
- Use the React.lazy() function to load components on-demand
- Use dynamic imports to load modules on-demand
import React, { lazy, Suspense } from "react";
// We define a lazy-loaded component using React.lazy().
const LazyComponent = lazy(() => import("./LazyComponent"));
// For components which don't have default export:
const LazyComponent = lazy(() => import('src/components/ROIChart').then((module) => ({ default: module.ROIChart })))
function MyComponent() {
return (
<div>
<h2>My Component</h2>
{/* We wrap the lazy-loaded component in a Suspense component. */}
<Suspense fallback={<div>Loading...</div>}>
{/* We render the lazy-loaded component. */}
<LazyComponent />
</Suspense>
</div>
);
}
export default MyComponent;
Minification and gzipping:
Minification is the process of removing unnecessary characters from your code, such as white space, comments, and formatting. Gzipping is the process of compressing your code to reduce its size. To use minification and gzipping, you should:
- Use a build tool like Webpack to minify and gzip your code. (JS, CSS, HTML)
- Use a Content Delivery Network (CDN) to serve your minified and gzipped code
Caching:
Caching is the process of storing frequently accessed data in memory so that it can be accessed quickly. This improves the performance of your application by reducing the amount of time it takes to load data. To use caching, you should:
- Use the browser cache to store frequently accessed resources
- Use a Service Worker to cache resources for offline access
Lazy loading:
Lazy Loading is a technique used to defer the loading of non-critical resources until they are needed. This improves the performance of your application by reducing the initial load time. To use lazy loading, you should:
- Load images, videos, and other non-critical resources lazily
- Use the Intersection Observer API to load resources when they become visible on the screen
Optimising images:
Images can significantly impact the performance of your application. To optimise images, you should:
- Use the right image format for each image
- Compress your images to reduce their size
- Use lazy loading
Optimisation of a React application is essential for a fast and responsive user experience. You can significantly improve the performance of your React application by following the best practices and tips discussed in this article.
To optimize images I prefer to use https://imagecompressor.com/
Also, remeber that Google loves .webp format so you can convert your image here https://convertio.co/jpg-webp/
That's all, folks!
Keep your components simple and focused, use pure components, store expensive calculations, avoid inline functions and object creation in rendering methods, use code splitting and lazy loading, minify and gzip, use caching, and optimise images.