Efficient React Components: A Guide to Optimizing React Performance
Why does a React web app run slow? The answer often lies in when and how frequently your components re-render, and whether those re-renders were even necessary. React doesn’t promise magical performance gains, but it provides just the right tools and functionalities to make it easy.
In this article, Toptal Freelance Software Engineer William Wang walks us through some optimization techniques that can help you build performant React web apps.
Why does a React web app run slow? The answer often lies in when and how frequently your components re-render, and whether those re-renders were even necessary. React doesn’t promise magical performance gains, but it provides just the right tools and functionalities to make it easy.
In this article, Toptal Freelance Software Engineer William Wang walks us through some optimization techniques that can help you build performant React web apps.
With experience ranging from freelancer to CTO, William is a full-stack developer witrh a background in the LAMP stack and React.js.
Expertise
Since its introduction, React has changed the way front-end developers think of building web apps. With virtual DOM, React makes UI updates as efficient as they can ever be, making your web app snappier. But, why do moderately sized React web apps still tend to perform poorly?
Well, the clue is in how you are using React.
A modern front-end library like React doesn’t magically make your app faster. It requires the developer to understand how React works and how the components live through the various phases of the component lifecycle.
With React, you can gain a lot of the performance improvements that it has to offer by measuring and optimizing how and when your components render, and the framework provides the tools necessary to facilitate React performance optimization.
In this tutorial we will focus on optimizing React performance. You will learn how you can measure the performance of your React components and optimize them to build a much more performant React web app. You will also learn how a few JavaScript best practices also help in making your React web app deliver a much more fluent user experience.
How Does React Work?
Before we dive into React optimization techniques, we need to have a better understanding of how React works.
At the core of React development, you have the simple and obvious JSX syntax, and React’s ability to build and compare virtual DOMs. Since its release, React has influenced many other front-end libraries. Libraries such as Vue.js also rely on the idea of virtual DOMs.
Here is how React works:
Each React application begins with a root component, and is composed of many components in a tree formation. Components in React are “functions” that render the UI based on the data (props and state) it receives.
We can symbolize this as F
.
UI = F(data)
Users interact with the UI and cause the data to change. Whether the interaction involves clicking a button, tapping on an image, dragging list items around, AJAX requests invoking APIs, etc., all those interactions only change the data. They never cause the UI to change directly.
Here, data is everything that defines the state of the web application, and not just what you have stored in your database. Even bits of front-end states (e.g., which tab is currently selected or whether a checkbox is currently checked) are part of this data.
Whenever there is a change in this data, React uses the component functions to re-render the UI, but only virtually:
UI1 = F(data1)
UI2 = F(data2)
React computes the differences between the current UI and the new UI by applying a comparison algorithm on the two versions of its virtual DOM.
Changes = Diff(UI1, UI2)
React then proceeds to apply only the UI changes to the real UI on the browser.
When the data associated with a component change, React determines if an actual DOM update is required. This allows React to avoid potentially expensive DOM manipulation operations in the browser, such as creating DOM nodes and accessing existing ones beyond necessity.
This repeated diffing and rendering of components can be one of the primary sources of React performance issues in any React app. Building a React app where the diffing algorithm fails to reconcile effectively, causing the entire app to be rendered repeatedly can result in a frustratingly slow experience.
Where to Start Optimizing?
But what exactly is it that we optimize?
You see, during the initial render process, React builds a DOM tree like this:
Given a part of the data changes, what we want React to do is re-render only the components that are directly affected by the change (and possibly skip even the diff process for the rest of the components):
However, what React ends up doing is:
In the image above, all of the yellow nodes are rendered and diffed, resulting in wasted time/computation resources. This is where we will primarily put our optimization efforts in. Configuring each component to only render-diff when it is necessary will allow us to reclaim these wasted CPU cycles.
Developers of the React library took this into consideration and provided a hook for us to do just that: a function that lets us tell React when it is okay to skip rendering a component.
Measuring First
As Rob Pike puts it rather elegantly as one of his rules of programming:
Measure. Don’t tune for speed until you’ve measured, and even then don’t unless one part of the code overwhelms the rest.
Do not start optimizing code that you feel may be slowing your app down. Instead, let React performance measuring tools guide you through the way.
React has a powerful tool just for this. Using the react-addons-perf
library you can get an overview of your app’s overall performance.
The usage is very simple:
Import Perf from 'react-addons-perf'
Perf.start();
// use the app
Perf.stop();
Perf.printWasted();
This will print a table with the amount of time components wasted in rendering.
The library provides other functions that let you print different aspects of the wasted time separately (e.g., using the printInclusive()
or the printExclusive()
functions), or even print the DOM manipulation operations (using the printOperations()
function).
Taking Benchmarking A Step Further
If you are a visual person, then react-perf-tool
is just the thing you need.
react-perf-tool
is based on the react-addons-perf
library. It gives you a more visual way of debugging performance of your React app. It uses the underlying library to get measurements and then visualizes them as graphs.
More often than not, this is a much more convenient way of spotting bottlenecks. You can use it easily by adding it as a component to your application.
Should React Update The Component?
By default, React will run, render the virtual DOM, and compare the difference for every component in the tree for any change in its props or state. But that is obviously not reasonable.
As your app grows, attempting to re-render and compare the entire virtual DOM at every action will eventually slow down.
React provides a simple way for the developer to indicate if a component needs re-rendering. This is where the shouldComponentUpdate
method comes into play.
function shouldComponentUpdate(nextProps, nextState) {
return true;
}
When this function returns true for any component, it allows the render-diff process to be triggered.
This gives the you a simple way of controlling the render-diff process. Whenever you need to prevent a component from being re-rendered at all, simply return false
from the function. Inside the function, you can compare the current and next set of props and state to determine whether a re-render is necessary:
function shouldComponentUpdate(nextProps, nextState) {
return nextProps.id !== this.props.id;
}
Using a React.PureComponent
To ease and automate a bit this optimization technique, React provides what is known as “pure” component. A React.PureComponent
is exactly like a React.Component
that implements a shouldComponentUpdate()
function with a shallow prop and state comparison.
A React.PureComponent
is more or less equivalent to this:
class MyComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this.props, nextProps) && shallowCompare(this.state, nextState);
}
…
}
As it only performs a shallow comparison, you may find it useful only when:
- Your props or states contain primitive data.
- Your props and states have complex data, but you know when to call
forceUpdate()
to update your component.
Making Data Immutable
What if you could use a React.PureComponent
but still have an efficient way of telling when any complex props or states have changed automatically? This is where immutable data structures make life easier.
The idea behind using immutable data structures is simple. Whenever an object containing complex data changes, instead of making the changes in that object, create a copy of that object with the changes. This makes detecting changes in data as simple as comparing the reference of the two objects.
You can use Object.assign
or _.extend
(from Underscore.js or Lodash):
const newValue2 = Object.assign({}, oldValue);
const newValue2 = _.extend({}, oldValue);
Even better, you can use a library that provides immutable data structures:
var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 2);
assert(map1.equals(map2) === true);
var map3 = map1.set('b', 50);
assert(map1.equals(map3) === false);
Here, Immutable.Map
is provided by the library Immutable.js.
Every time a map is updated with its method set
, a new map is returned only if the set operation changed the underlying value. Otherwise, the same map is returned.
You can learn more about using immutable data structures here.
More React Performance Optimization Techniques
Using the Production Build
When developing a React app, you are presented with really useful warnings and error messages. These make identifying bugs and issues during development a bliss. But they come at a cost of performance.
If you look into React’s source code, you will see a lot of if (process.env.NODE_ENV != 'production')
checks. These chunks of code that React is running in your development environment isn’t something needed by the end user. For production environments, all of this unnecessary code can be discarded.
If you bootstrapped your project using create-react-app
, then you can simply run npm run build
to produce the production build without this extra code. If you are using Webpack directly, you can run webpack -p
(which is equivalent of webpack --optimize-minimize --define process.env.NODE_ENV="'production'"
.
Binding Functions Early
It is very common to see functions bound to the context of the component inside the render function. This is often the case when we use these functions to handle events of child components.
// Creates a new `handleUpload` function during each render()
<TopBar onUpload={this.handleUpload.bind(this)} />
// ...as do inlined arrow functions
<TopBar onUpload={files => this.handleUpload(files)} />
This will cause the render()
function to create a new function on every render. A much better way of doing the same is:
class App extends React.Component {
constructor(props) {
super(props);
this.handleUpload = this.handleUpload.bind(this);
}
render() {
…
<TopBar onUpload={this.handleUpload} />
…
}
}
Using Multiple Chunk Files
For singl-page React web apps, we often end up bundling all of our front-end JavaScript code in a single minified file. This works fine for small to moderately sized web apps. But as the app starts to grow, delivering this bundled JavaScript file to the browser in itself can become a time consuming process.
If you are using Webpack to build your React app, you can leverage its code splitting capabilities to separate your built app code into multiple “chunks” and deliver them to the browser on an as-needed basis.
There are two type of splitting: resource splitting and on-demand code splitting.
With resource splitting, you split resource content into multiple files. For example, using CommonsChunkPlugin, you can extract common code (such as all external libraries) into a “chunk” file of its own. Using ExtractTextWebpackPlugin, you can extract all CSS code into a separate CSS file.
This kind of splitting will help in two ways. It helps the browser to cache those less frequently changing resources. It will also help the browser to take advantage of parallel downloading to potentially reduce the load time.
A more notable feature of Webpack is on-demand code splitting. You can use it to split code into a chunk that can be loaded on-demand. This can keep the initial download small, reducing the time it takes to load the app. The browser can then download other chunks of code on-demand when needed by the application.
You can learn more about Webpack code splitting here.
Enabling Gzip on Your Web Server
React app’s bundle JS files are commonly very big, so to make the web page load faster, we can enable Gzip on the web server (Apache, Nginx, etc.)
Modern browsers all support and automatically negotiate Gzip compression for HTTP requests. Enabling Gzip compression can reduce the size of the transferred response by up to 90%, which can significantly reduce the amount of time to download the resource, reduce data usage for the client, and improve the time to first render of your pages.
Check the documentation for your web server on how to enable compression:
- Apache: Use mod_deflate
- Nginx: Use ngx_http_gzip_module
Using Eslint-plugin-react
You should use ESLint for almost any JavaScript project. React is no different.
With eslint-plugin-react
, you will be forcing yourself to adapt to a lot of rules in React programming that can benefit your code on the long run and avoid many common problems and issues that occur due to poorly written code.
Make Your React Web Apps Fast Again
To make the most of React, you need to leverage its tools and techniques. A React web app’s performance lies in the simplicity of its components. Overwhelming the render-diff algorithm can make your app perform poorly in frustrating ways.
Before you can optimize your app, you will need understand how React components work and how they are rendered in the browser. The React lifecycle methods give you ways to prevent your component from re-rendering unnecessarily. Eliminate those bottlenecks and you will have the app performance your users deserve.
Although there are more ways of optimizing, when it comes to React performance improvements, fine tuning the components to update only when required yields the best results.
Share your thoughts on React performance best practices in the comments below.
Further Reading on the Toptal Blog:
- Stale-While-Revalidate Data Fetching With React Hooks: A Guide
- Heavy Computation Made Lighter: React Memoization
- Building Simple and Efficient Components With React-Bootstrap
- React Router Tutorial: Redirect Like a Pro
- The Secret Behind Steady App Performance: Asynchronous JavaScript
- Next.js vs. React: A Comparative Tutorial
- SolidJS vs. React: The Go-to Guide
- Tested Solutions: Working With React Design Patterns
Understanding the basics
What React component should you optimize first?
You should always begin my measuring the performance of your code. Sometimes seemingly “simple” code can be the cause of your app slowing down. Measure the performance of your React code with the tools it has to offer before diving into optimizing it.
How do I use React.PureComponent?
Unlike React.Component, a React.PureComponent will only re-render when there’s a change in the state or props of the component. It performs a shallow comparison to detect changes. You can leverage this behavior using immutable data structures in the state that changes reference whenever its contents are changed.
Kunming, Yunnan, China
Member since December 12, 2016
About the author
With experience ranging from freelancer to CTO, William is a full-stack developer witrh a background in the LAMP stack and React.js.