Many developers ask: What can I do to make my React app faster? How can I increase performance and reduce lag? Is it possible to optimize my React app for better performance?
Performance optimization is an important topic in web development, and React is no exception. Poorly optimized React apps can suffer from slow loading times and decreased responsiveness, leading to a poor user experience. Studies have shown that even small improvements in page loading times can lead to a significant increase in user engagement.
By the end of this article, you’ll have the knowledge to optimize your React app for better performance. You’ll learn about techniques such as code splitting, lazy loading, caching, and more. You’ll also become familiar with tools and libraries that can help you optimize your React app. So let’s dive in and get started!
10+ Ways to Optimize the Performance of a React App
If you have a small and stable React app and it works well, there is little sense in optimizing it. You need to improve software performance if you have problem areas, your app loads slowly, and it takes approximately 100-140 milliseconds to process events. According to Unbounce, page speed is a critical factor in buying behavior. Therefore, the performance of a system directly affects the prosperity of a business. Thus, its improvement can become a software project rescue. There are at least 10 ways to improve a React app.
Method 1. Reduce the number of element redraws
In an app written in React, you can often observe additional redrawing when clicking on interface elements. Each character input takes a lot of time due to re-rendering. If you optimize the code correctly, you can significantly reduce the time to process events.
React offers 3 optimization methods:
- shouldComponentUpdate;
- React.PureComponent;
- React.memo.
It is useful to implement the shouldComponentUpdate
lifecycle method like this:
shouldComponentUpdate(nextProps){
if (this.props.color !== nextProps.color){
return true;
}
return false;
}
That is, we receive some following props and compare them with the current ones. If it is necessary to redraw something, then we return true
. If not, we return false
and the component is not rerendered.
React.PureComponent
is a ready-made shouldComponentUpdate
that performs a shallow comparison and decides whether an element needs to be redrawn or not. See an implementation instance below:
class Table extends React.PureComponent {
render() {
return …
}
}
With React.PureComponent
, there will be noticeably less redrawing. This happens because, during a shallow comparison, a new function is created each time when we write function calls in elements. And the shallow comparison considers that we have passed a new property and causes our component to redraw. That is, with the shallow comparison feature, new arrays, objects, and functions return false
each time, and this causes redrawing:
class ListContainer extends Component {
render() {
return (
<List
style={‘background’: ‘red’}}
onClick={ () => showMessage (‘Hello’)}
showInStatus={[‘load’, ‘completed’]}
To fix the problem, it is important to cache and put everything in the body of the object. Now the elements will not be perceived as new objects, functions, or arrays each time but as links to current objects and functions. See the corrected version:
class ListContainer extends Component{
style={‘background’: ‘red’}
onClick= () => showMessage (‘Hello’);
showInStatus={[‘load’, ‘completed’];
render() {
return (
<List
style={this.style}
onClick={this.onClick}
showInStatus={this.showInStatus}
/>
)}}
If you need to cache methods with a parameter, the memoizeOne
function will help you. It remembers the only state (the last one), but we don’t need more states to work with redrawing. After all, if the property has been modified, we will have to redraw it. We suggest practicing this code for caching a method with a parameter:
class ListContainer extends Component {
onClick = memoizeOne(
(name) => showMessage(‘Hello ${name}’)
);
render() {
return (
<List
onClick={this.onClick(this.props.name)}
/>
)}}
React.memo
is an analog for functional components. It also performs a shallow comparison by default. But as the second argument, you can pass your own comparison function, in which we will decide whether an element needs to be redrawn or not. The code might look like this:
function MyComponent(props) {
return (...)
}
function areEqual(prevProps, nextProps) {
// we return false if the component needs to be rendered
}
const MyMemoComponent = React.memo(MyComponent, areEqual);
By using these methods, you will get rid of unnecessary drawings of elements on a page and get faster processing of events.
Method 2. Split React code with dynamic imports
Merging all code files is necessary so that a page can handle fewer HTTP requests. However, as the app grows, the file sizes increase and, as a result, the size of the batch file grows. The larger the file becomes, the slower the page loads. This is not very pleasing to people who are used to the fast operation of the platform.
But there is also a way out of this situation. It is possible to divide a huge file into multiple parts using dynamic imports.
To execute code splitting, use the following function to modify the normal React imports:
import Home from "./components/Home";
import About from "./components/About";
And then you need to “ask” React to dynamically load the elements:
const Home = React.lazy(() => import("./components/Home"));
const About = React.lazy(() => import("./components/About"));
This code change will allow React to load a file only for the requested page rather than a batch file for an entire app.
After importing, it is necessary to display lazy components inside the Suspense component by carrying out the following code:
<React.Suspense fallback={<p>Loading page...</p>}>
<Route path="/" exact>
<Home />
</Route>
<Route path="/about">
<About />
</Route>
</React.Suspense>
With Suspense
, loading text or an indicator is displayed as a fallback while React is waiting to render a lazy component in the UI.
Method 3. Use the Reselect library to prevent re-rendering
Imagine that you have a store with usernames and their roles in the app. To get all administrators from the list of users, you need to run the following code:
const mapStateToProps = ({users}) => ({
usersWithRoleAdmin: users.filter(
user => user.role === ‘admin’
)
});
But the “filter” function returns a new array each time, and the app will render again.
To fix this, you can use Reselect
, a special library of selectors. It allows you to cache calls and receive data depending on whether a state
has changed or not. It will return a reference to the same array, and everything will work correctly:
import {createSelector} from ‘reselect’;
const getUsersWithRoleAdmin = () => createSelector(
state => state.users,
users => users.filter(
user => user.role === ‘admin’
)
);
const mapStateToProps = (state) => ({
usersWithRoleAdmin: getUsersWithRoleAdmin(state)
});
Method 4. Don’t forget about options in the Connect function
In the Connect
options, some features are important for optimization – pure
and areStatesEqual
. It means that all components that are wrapped in Connect
become clean in fact. When we connect a component, it makes no sense to make this component pure, because all checks will pass in Connect
. So we remove the unnecessary comparison. The same will happen in Connect
:
@connect()
class Container extends Component {
render(){
…
}
}
By default, the areStatesEqual
function compares the previous store
values to the next store
values. If a store
has changed, then connect
calls all selectors and updates props
. Imagine a project with hundreds of connects, and every time we have all the selectors twitching. If we are working in one module, then it makes no sense for us to run all selectors, even if they are cached. We can optimize this issue in the following way:
@connect()
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
areStatesEqual: (next, prev) => prev.table === next.table
}
)
class Container extends Component{
…
}
In the function, we indicate the parts of the repository to whose changes we want to subscribe. If we are working in the same module and it has changed, then the selectors will work. If changes have occurred in other modules, selectors will be called in them, and not in all parts of the app.
Method 5. Use Web Workers to solve time-consuming tasks
The extensive computational operations that React software performs also affect the speed of a program. Use a special mechanism – Web Workers
. These are APIs that allow you to create separate threads for executing code. Web Workers
execute code on a separate thread for tasks that take a long time and require a lot of processor resources. Thus, they improve page performance. However, they don’t break the main thread, leaving the interface available to the user.
Imagine that we need to sort posts by the number of comments. This seemingly simple task can be overwhelming if it is necessary to sort through tens of thousands of messages. This situation will slow down rendering that is running on the same thread.
If we run the sort
method on a separate thread using Web Workers
, we will not interfere with the work of the main thread. The software will work stably even during complex calculations. Web Workers
is an excellent solution when you need to process a large number of images, filter information, or perform other operations that require a lot of CPU resources.
Method 6. Use libraries for list virtualization
If your React app has long lists of data (hundreds or thousands of rows long), the app will render them all. Accordingly, this procedure will take a lot of time. Don’t forget to use the react-window
or react-virtualized
libraries to speed up the process and not perform unnecessary operations.
These methods allow you to display only the part visible to the user in the DOM. Then, as you scroll, the remaining list items are displayed, replacing the ones that go out of the viewport. This action reduces the time required to render the original view and process updates. Methods reduce memory footprint without using additional DOM nodes.
See an example of data list virtualization:
import React, { Component } from 'react';
import ListComponent from './ListComponent';
class App extends Component {
render() {
return (
<>
<ListComponent />
<div className="title">
<h1>FixedSizeList | react-window</h1>
</div>
</>
);
}
}
export default App;
The FixedSizeList
component should be used for long one-dimensional lists with elements of the same size. For lists with elements of different sizes, you must use VariableSizeList
. When you need to virtualize a large table, use FixedSizeGrid
and VariableSizeGrid
.
With react-virtualized
, the virtual scroll is rendered from the bottom and removes the content from the top when scrolling down or from the bottom when scrolling up. The DOM size stays constant and the interface is responsive. But this method is suitable for flat lists. It is difficult to apply it to hierarchical ones. After all, we need to know the approximate size of the elements so that we can calculate which of them fall into the visible area, and which ones do not.
Method 7. Set up lazy loading of images
In the previous method, we discussed list rendering optimization. There is a similar solution for React apps where you need to load and display a large number of images at the same time. To reduce page load, the react-lazyload
library is used, which provides lazy loading.
The react-lazyload
option allows you to load objects asynchronously. We can wait for each image to appear in the viewport before rendering them to the DOM. This way unnecessary DOM nodes are not created, and the app runs faster.
Lazy loading looks like this:
const LazyLoadImage = ({
alt,
src,
className,
loadInitially = false,
observerOptions = { root: null, rootMargin: '220px 0px' },
...props
}) => {
const observerRef = React.useRef(null);
const imgRef = React.useRef(null);
const [isLoaded, setIsLoaded] = React.useState(loadInitially);
const observerCallback = React.useCallback(
entries => {
if (entries[0].isIntersecting) {
observerRef.current.disconnect();
setIsLoaded(true);
}
},
[observerRef]
);
React.useEffect(() => {
if (loadInitially) return;
if ('loading' in HTMLImageElement.prototype) {
setIsLoaded(true);
return;
}
observerRef.current = new IntersectionObserver(
observerCallback,
observerOptions
);
observerRef.current.observe(imgRef.current);
return () => {
observerRef.current.disconnect();
};
}, []);
return (
<img
alt={alt}
src={isLoaded ? src : ''}
ref={imgRef}
className={className}
loading={loadInitially ? undefined : 'lazy'}
{...props}
/>
);
};
ReactDOM.render(
<LazyLoadImage
src="image url"
alt="alt text"
/>,
document.getElementById('root')
);
Method 8. Group child elements with React.Fragments
Typically, when combining child elements in React, an additional DOM node is created. However, this additional node affects the performance of the app and sometimes provokes other problems (for example, incorrect formatting of the HTML output
element).
React.Fragments gives
the advantage of grouping a list of child elements without adding more nodes to the DOM:
class Comments extends React.PureComponent{
render() {
return (
<React.Fragment>
<h1>Comment </h1>
<p>comment A</p>
<p>comment B</p>
</React.Fragment>
);
}
}
React Fragments
lets you group a set of related components without having to add needless wrappers to the HTML. This results in your being able to write cleaner React elements and saves time when crafting layouts.
Method 9. Remove redundant methods from third-party developers
Before releasing a React product, it is worth checking and analyzing the app package to remove unnecessary plug-ins. When doing this, check how much code is used from third-party dependencies. For example, you are working on a project with the Lodash
library. It has over 100 methods available, but you only need 25. The remaining 75 methods will be in the app package even if you don’t use them. To reduce the package size, it is worth using the lodash-webpack-plugin
to remove unused functions.
You can use the Webpack Bundle Analyzer
to see what is in the app package, which modules “weigh” more, which ones were mistakenly included in the package, and which ones should be removed.
Method 10. Set up Gzip compression in the back-end
To speed up your React app, you can optimize server-side processes. Smashing Magazine researchers found that SEB (site experience benchmark) for sites with server-side rendering is 47% higher than for sites with client-side rendering.
This means that for slow sites, it is worth trying Gzip compression in the back-end. This will allow faster data fetching and client service, resulting in faster component loading and rendering times.
Gzip compresses files. So the server provides a smaller file, and the site loads faster. Gzip compresses common strings, so it can reduce the page size and style sheets by up to 60-70%, shortening the time of the first website rendering.
Here is an example of how to compress the bundle size in the Node/Express
back-end using the compression module:
const express = require('express');
const compression = require('compression');
const app = express();
// Pass `compression` as a middleware!
app.use(compression());
Conclusion
Such properties of React as high performance and the ability to constantly improve made it possible to use this framework to create apps of any complexity. React is the right foundation for building financial software, marketing platforms, logistics solutions, insurance websites, and telemedicine tools.
You should not optimize any React app in this way. This applies to complex large-scale websites that have serious performance issues. Therefore, you should first identify the problem and assess its nature. Then determine which of the above methods will help you to speed up your app. And then you can make the right decision to optimize the platform.