5 Easy ways to quickly optimize performance on ReactJS
What is Performance Optimization in ReactJS?
In the most straightforward terms, performance optimization simply means enhancing the responsivity of an app built on ReactJS. This is crucial for ReactJS app development too. There are several ways in which this is achieved in React. There is extensive documentation available, presented by the React Team, on this matter. In this article, we will discuss and try to understand the workings of some of the most efficient performance optimizing techniques in ReactJS.
Is Performance Optimization necessary in ReactJS?
Performance optimization is often not the biggest concern while developing using ReactJS. This is because ReactJS as a library hugely prioritizes the high responsiveness of apps. The responsivity of apps forms an important performance parameter while developing with React. Responsivity refers to the speed with which apps respond to user inputs. To make apps highly responsive, React has employed some rather novel, and popular features like virtual DOM in the library. Virtual DOMs are simply virtual copies of the real DOMs which results in faster user interfaces for React apps. This is achieved as Virtual DOMs eliminate the necessity to download all components on a page multiple times.
Nevertheless, as developers, we always aim to have better performance for our apps. There are a number of ways in which we can further increase the responsivity and speed of our UIs in React. We shall discuss some of the key performance optimization techniques in ReactJS in this article.
Ways to Optimize Performance in React Quickly
1. Using Production Build
First, we need to ask, what is production build? Production build in React allows you to use built-in optimizations. Although one would opt for development mode while working on building the UI, it is advisable to use production build while deploying the UI, especially if you are facing performance issues. There is a production build present in Create React App, Brunch, Browserify, Rollup, Webpack, and so on. Before delving into how to employ production build, let us first understand what production build does for our apps.
What does production build do? The production build produces streamlined assets, leaner source maps, and minifies bundles. This speeds up the time of UI loads.
When we are building a UI, it is useful to do it in the development mode of React. In the development mode, React provides us with warnings that enable the detection of bugs during development itself. However, the development mode also results in bulking up the bundle size for our apps. This slowing down does not pose any issues while we are developing and running the app locally, but it would inevitably also pose a similar slowdown during the app deployment. We cannot afford this during app deployment. Thus, we must use production build during deploying our apps.
Let us see how to use production build in React.
create-react-app
First, we launch a production build of our app, using the command:
1
npm run build
This production build of the app can then be simply deployed from the build/ folder of our project.
Brunch
The terser-brunch plugin offers the most effective production build for our React apps.
1
2
# If you use npm
npm install --save-dev terser-brunch
1
2
# If you use Yarn
yarn add --dev terser-brunch
Once the plugin is installed we can create a production build for our apps by introducing the -p flag:
1
brunch build -p
Browserify
Much like Brunch, for Browserify too, we need to first install the necessary plugin as follows:
1
2
# If you use npm
npm install --save-dev envify terser uglifyify
1
2
# If you use Yarn
yarn add --dev envify terser uglifyify
Now, we can create the production build of our app once we set the proper build environment and then remove the development imports globally, using the -g flag:
1
2
3
4
browserify ./index.js \
-g [ envify --NODE_ENV production ] \
-g uglifyify \
| terser --compress --mangle > ./bundle.js
Rollup
We follow the same drill with Rollup as well. First, we install the plugin, followed by creating the production build of the app after setting the proper build environment followed by removing the development imports globally.
Installing plugin:
1
2
# If you use npm
npm install --save-dev rollup-plugin-commonjs rollup-plugin-replace rollup-plugin-terser
1
2
# If you use Yarn
yarn add --dev rollup-plugin-commonjs rollup-plugin-replace
Creating app production build:
1
2
3
4
5
6
7
8
9
plugins: [
// ...
require('rollup-plugin-replace')({
'process.env.NODE_ENV': JSON.stringify('production')
}),
require('rollup-plugin-commonjs')(),
require('rollup-plugin-terser')(),
// ...
]
Webpack
The Webpack 4+ automatically minifies our app in the production build, enhancing loading speed several folds. To create a production build of the app in webpack:
1
2
3
4
5
6
7
8
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
optimization: {
minimizer: [new TerserPlugin({ /* additional options here */ })],
},
};
Please note: If you do not want React to hide warnings while building your app, create the production build of your app only when you are ready to deploy it. Developing apps in production build might end up in further slowing down of apps due to bugs.
2. Using Profilers for Component Profiling
Let us first understand what is profiling. Profiling in React means measuring the frequency at which components are rendered in an app as well as the cost of rendering them. Components are the building blocks of React UIs. HTML tags are added to components in React which then act as DOMs. Naturally, optimizing components can add up to the performance optimization of your apps. This is precisely what profiling is. Profilers can identify components that are causing a slowdown of the app and offer optimizations like memoization to eliminate performance issues.
React provides us with the React DevTools Profiler for profiling components in DEV mode, that is development mode. DevTools Profiler can perform advanced component profiling through react-dom 16.5+ and react-native 0.57+. Additionally, you can also use react-dom/profiling, a production profiling bundle for react-dom. You can readily install DevTools as a browser extension and get to optimize the performance of your apps in development mode.
3. Windowing Large Data
When your application is required to render long lists of data, we are talking in the range of hundreds or even thousands of rows of data, a technique called windowing might be necessary for optimizing the performance of your apps. Apps like social media apps or e-commerce apps often have to deal with such a large amount of data. This considerably threatens the loading speed of the app, something social media apps or e-commerce apps especially cannot afford. This is where windowing comes into play.
What is windowing? In ReactJS, windowing makes it possible to render smaller subsets of the data rows at specific time points. This dramatically reduces the re-rendering time of components. Simultaneously, it reduces the number of DOM nodes produced. Naturally, as a smaller amount of data is rendered at any given time, the performance of the app is enhanced and a slowdown is avoided.
React offers us in-built windowing libraries. Of these, you can use react-window and react-virtualized to effectively window your data. These windowing libraries consist of many reusable components that are functional for efficiently displaying your data organized into tables, grids, or lists.
4. Limiting Reconciliation
What is reconciliation in ReactJS? To put it simply, reconciliation is the process of updating virtual DOMs in React. Unnecessary virtual DOM updates can have a significantly adverse effect on the performance of your app. Therefore, ensuring that virtual DOMs are updated only when necessary can act as a performance optimization technique for you.
We have often had the term “virtual DOM” thrown at us nearly all the time we read or discuss matters involving ReactJS. We might as well brush up on our concept of virtual DOMs before we delve into understanding how reconciliation is involved with performance optimization in React.
React creates as well as updates a conceptual model of the displayed user interface. It comprises the React elements that components of your UI return. Since operating on DOM nodes might take a longer time compared to operating on JavaScript objects, this approach enables React to avoid creating new DOM nodes along with accessing those that are already present. These are referred to as browser DOMs, or more commonly, "virtual DOM".
React determines if a real DOM update is required whenever a component's props or state is altered. This is achieved by evaluating the immediately returned element and the earlier rendered elements. React only updates the DOM if the values are not equivalent.
Re-rendering can prove to be a time-consuming process even though React merely updates the modified DOM nodes. When this process noticeably reduces the speed of the app, we can opt for overriding a lifecycle function that is activated during re-rendering. This function is called shouldComponentUpdate. The following image shows how the decision of re-rendering DOMs is made by the ‘Diffing’ algorithm of React.
5. Using Immutable Data Structure
Using immutable data while coding is more of a coding style. While some developers choose to use mutable data in their states and props, some swear by using immutable data. Regarding performance optimization, you can consider using an immutable data structure as a potent technique to enhance the performance of your apps. However, it is an overall good coding practice, a trick of trade even, to use immutable data structures while programming. The idea behind using an immutable data structure for performance optimization is quite straightforward. Consider yourself as a processing unit. If you were to handle data sets that were constantly changing in comparison to data that did not change as frequently, which would take you lesser time to process? It is easier, and therefore faster to handle data that is not subject to continuous change. Similarly, for React, using immutable, which is an unchangeable data structure for states and props enhances the performance of your code by several folds. Additionally, this does not require any extra effort or external APIs and instead, takes you less time to code as well as monitor your program.
First, let us see an example of how you can use immutable data instead of mutable data by comparing the same code written in two different styles. Let us consider that we have an object named colormap. We are going to write a code to make colormap.right red.
1
2
3
function updateColorMap(colormap) {
colormap.right = 'red';
}
Seems pretty straightforward, right? We can, however, make this even easier while making colormap immutable in the process, by simply using Object.assign. This will make updateColormap return a new object in red, instead of mutating the old colormap object:
1
2
3
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'red'});
}
This was a simple example of how we can simply write our codes from a different perspective, or if I may say it as a different style, to avoid mutable data structure.
However, there are also a few in-built ways to avoid using mutable data in your states and props in React. One of them is using concat:
1
2
3
4
5
handleClick() {
this.setState(state => ({
words: state.words.concat(['marklar'])
}));
}
Additionally, if you are using create-react-app, you can avail of the spread syntax for arrays automatically. This syntax is supported by EcmaScript6 in React. In this way, you would not have to use concat separately.
1
2
3
4
5
handleClick() {
this.setState(state => ({
words: [...state.words, 'marklar'],
}));
};
Examples of Performance Optimization in ReactJS
In this section, we will consider a few sample cases involving performance optimization using the techniques we discussed in the above section.
Case 1: shouldComponentUpdate
You can have shouldComponentUpdate verify that if the only time your components update is whenever the props.color or state.count variables change. In this scenario shouldComponentUpdate only evaluates to see if props.color or state.count have been updated. The component will never update if the values stay equivalent. In this way, unnecessary reconciliation is avoided, thus enhancing performance.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
Case 2: React.PureComponent
This too is another method to avoid unnecessary reconciliation in React. React.PureComponent comes into play when your code is too complex for shouldComponentUpdate to handle. If anything, React.PureComponent makes your complex code simpler, besides optimizing performance by avoiding unnecessary updates. To evaluate whether the component should update when becomes more complicated, we use React.PureComponent, a pre-built logic offered by React. It helps React in performing a "shallow comparison" across all the variables of props and state.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
Case 3: React.Fragments
As we discussed in a previous section, creating new nodes can significantly reduce app performance, especially when large datasets are associated with the apps. We discussed employing a technique called windowing to get around this issue. React.fragments is an example of windowing at play. It simply allows us to group a large number of children without creating extra nodes.
1
2
3
4
5
6
7
8
9
10
11
class Comments extends React.PureComponent{
render() {
return (
<React.Fragment>
<h1>Comment Title</h1>
<p>comments</p>
<p>comment time</p>
</React.Fragment>
);
}
}
A Quick Recap
To wrap things up, we discussed in this article the major ways to optimize performance in your React apps. The React library takes performance enhancement of apps quite seriously and thus offers developers a wide range of tools in the form of inbuilt syntax, functions, or external libraries and APIs that can be used to enhance the performance of apps. Note that, while using some of these techniques in all your React projects, like using an immutable data structure, or deploying apps in a production build, would be considered good coding practice. However, some of the other techniques would be necessary when your app is showing noticeable performance issues.
In this article, we discussed the production build of apps, profiling, windowing, reconciliation, and immutable data structures in terms of performance optimization. These are the main techniques in React for performance optimization that can also be deployed quickly. Nevertheless, there are numerous other small coding tricks that we can use to enhance the performance of our apps in React. We shall discuss them in another article.
Sam took the long path into the world of IT. A post-grad in Bioinformatics, she started coding to build better programs for protein sequencing. Currently, Sam actively writes blogs for 4 Way Technologies. She's always excited about all the new technologies and updates in software development. Sam also writes coding tutorials and beginners guides for, well, beginners.