Have you ever wondered how to make your React applications faster?
Yes?
How about having a checklist for eliminating common React performance issues?
Well, you are in the right place.
In this article, I’ll walk you through a pragmatic, step-by-step guide to eliminating common React performance issues.
First, I’ll show you the problems and then provide solutions to them. In doing this, you’ll be able to carry over the same concepts to your real-world projects.
The goal of this article is not to be a lengthy essay, rather, I’ll discuss quick tips you can start using in your applications today.
Let’s get started!
To make this article as pragmatic as possible, I’ll walk you through various use cases while working on a single React application.
I call this app Cardie.
Here’s the GitHub repo if you want to follow along.
Cardie is a simple application. All it does is display the profile information of a user. Commonly known as a user profile card.
It also includes the added functionality of being able to change the user’s profession with a button click.
Upon clicking the button at the bottom of the app, the user’s profession is changed.
While you may be quick to laugh this off as a simple application, and nowhere near a real-world app, you may be surprised to know that the knowledge gained from hunting down performance issues with this example application applies to whatever real-world app you build.
So, stay calm and enjoy the article.
Here comes the checklist!
Identifying wasted renders in your React applications is the perfect start to identifying most performance issues.
There are a couple different ways to approach this, but the simplest method is to toggle on the “highlight updates” option in the React DevTools.
While interacting with the app, updates are highlighted on the screen with a green flash.
The green flash shows the components in the app that are re-rendered by React under the hood.
With Cardie, upon changing the user’s profession, it appears that the entire parent component App
is re-rendered.
That doesn’t look right.
The app works, but there’s really no reason for the entire component to be re-rendered when the change to be updated is in one small bit.
A more ideal highlighted update should look like this:
In more complex applications, the impact of a wasted render may be huge! The re-rendered component may be large enough to promote performance concerns.
Having seen the problem, are there any solutions to this?
Once you’ve visually noted wasted renders in your application, a good place to start is to attempt to break up your component tree to support regions updated frequently.
Let me show you what I mean.
In Cardie, the App
component is connected to the Redux store via the connect
function from react-redux
. From the store, it receives the props name
, location
, likes
, and description
.
The description
props define the current profession of the user.
Essentially, what’s happening is that whenever the user profession is changed by clicking the button, the description
prop is changed. This change in props then causes the App
component to be re-rendered entirely.
If you remember from React 101, whenever the props
or state
of a component changes, a re-render is triggered.
Instead of allowing the App
component to re-render pointlessly, what if we localized the elements being updated to a specific React component?
For example, we could create a new component called Profession
which renders its own DOM elements.
In this case, the Profession
component will render the description of the user’s profession, e.g., “I am a Coder”.
The component tree would now look like this:
What’s also important to note here is that instead of letting <App/>
worry about the profession
prop, that’ll now be a concern for the <Profession/>
component.
Whether you use redux
or not, the point here is that App
no longer has to be re-rendered owing to changes from the profession
prop. Instead, the <Profession/>
component will be.
Upon completing this refactor, we have an ideal update highlighted as seen below:
To view the code change, please see the isolated-component branch from the repo.
Any React resource talking about performance will very likely mention pure components.
However, how do you know when to properly use pure components?
Well, it is true that you could make every component a pure component, but remember there’s a reason why that isn’t the case out of the box — hence, the shouldComponentUpdate
function.
The premise of a pure component is that the component only re-renders if the props
are different from the previous props and state.
An easy way to implement a pure component is to use React.PureComponent
as opposed to the default React.Component
.
To illustrate this specific use case in Cardie, let’s break down the render elements of the Profession
component into smaller components.
So, this was the content of Profession
before now:
const Description = ({ description }) => {
return (
<p>
<span className="faint">I am</span> a {description}
</p>
);
}
Here’s what we’ll change it to:
const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};
Now, the Description
component renders 4 children components.
Note that the Description component takes in a profession
prop. However, it passes on this prop to the Profession
component. Technically, none of the other 3 components care about this profession
prop.
The contents of these new components are super simple. For example, the <I />
component just returns a span
element with an “I” text: span >I </span>
If the application runs, the result is just the same. The app works.
What’s interesting is that upon a change to the description
prop, every child component of Profession
is also re-rendered.
I added a few logs in the render
methods of each child component – and as you can see they were indeed all re-rendered.
You may also view the highlighted updates using the react dev tools.
This behavior is expected. Whenever a component has either props
or state
changed, the tree of elements it renders is recomputed. This is synonymous to a re-render.
In this particular example, you’ll agree with me that if really makes no sense for <I/>
, <Am/>
and <A/>
child components to be re-rendered. Yes, the props
in the parent element, <Description/>
changed, but if this were a sufficiently large application, this behavior may pose some performance threats.
What if we made these child components pure components?
Consider the <I/>
component:
import React, {Component, PureComponent} from "react"
//before
class I extends Component {
render() {
return <span className="faint">I </span>;
}
//after
class I extends PureComponent {
render() {
return <span className="faint">I </span>;
}
By implication, React is informed under the hood so that if the prop values for these child components aren’t changed, there’s no need to re-render them.
Yes, do not re-render them even when the parent element has a change in its props!
Upon inspecting the highlighted updates after the refactor, you can see that the child components are no longer re-rendered. Just the Profession
component whose prop
actually changes is re-rendered.
In a larger application, you may find immense performance optimizations by just making certain components pure components.
To view the code change, please see the pure-component branch from the repo.
Remember again that whenever the props
for a component changes, a re-render happens.
What if the props
for your component didn’t change but React thinks it did change?
Well, there’ll also be a re-render! But isn’t that weird?
This seemingly weird behavior happens because of how Javascript works & how React handles its comparison between old and new prop values.
Let me show you an example.
Here’s the content for the Description
component:
const Description = ({ description }) => {
return (
<p>
<I />
<Am />
<A />
<Profession profession={description} />
</p>
);
};
Now, we will refactor the I
component to take in a certain i
prop. This will be an object of the form:
const i = {
value: "i"
};
Whatever value property is present in i
will be set as the value in the I
component.
class I extends PureComponent {
render() {
return <span className="faint">{this.props.i.value} </span>;
}
}
In the Description
component, the i
prop is created and passed in as shown below:
class Description extends Component {
render() {
const i = {
value: "i"
};
return (
<p>
<I i={i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}
Bear with me while I explain this.
This is perfectly correct code, and it works fine — but there is one problem.
Even though I
is a pure component, now it re-renders whenever the profession of the user is changed!
But why?
As soon as the Description
component receives the new props, the render
function is called to create its element tree.
Upon invoking the render
function, it creates a new i
constant:
const i = {
value: "i"
};
When React gets to evaluate the line <I i={i} />
, it sees the props i
as a different prop, a new object — hence the re-render.
If you remember from React 101, React does a shallow comparison between the previous and next props.
Scalar values such as strings and numbers are compared by value. Objects are compared by reference.
By implication, even though the constant i
has the same value across re-renders, the reference is not the same. The position in memory isn’t.
It’s a newly created object with every single render call. For this reason, the prop
value passed to <I/>
is regarded as “new,” consequently producing a re-render.
In bigger applications, this can lead to a wasted render and potential performance pitfalls.
Avoid this.
This applies to every prop
, including event handlers.
If you can avoid it, you shouldn’t do this:
...
render() {
<div onClick={() => {//do something here}}
}
...
You’re creating a new function object every time within render. Better to do this:
...
handleClick:() ={
}
render() {
<div onClick={this.handleClick}
}
...
Got that?
In the same vein, we may refactor the prop sent to <I />
as shown below:
class Description extends Component {
i = {
value: "i"
};
render() {
return (
<p>
<I i={this.i} />
<Am />
<A />
<Profession profession={this.props.description} />
</p>
);
}
}
Now, the reference is always the same, this.i
.
A new object isn’t created at render time.
To view the code change, please see the new-objects branch from the repo.
When deploying to production, always use the production build. This is a very simple but great practice.
If you have bootstrapped your application with create-react-app
to run the production build, use the command npm run build
.
This will yield bundle-optimized files for production.
When you bundle your application, you likely have the entire application bundled in one large chunk. The problem with this is that as your app grows, so does the bundle.
Code splitting advocates that instead of sending this large chunk of code to users at once, you may dynamically send chunks to users when they need it.
A common example is with route-based code splitting. In this method, the code is split into chunks based on the routes in the application.
Another approach is component-based code splitting. In this method, if a component is currently not displayed to the user, its code may be delayed from being sent to the user.
Whichever method you stick to, it is important to understand the trade-offs and not degrade the user experience of your application.
Code splitting is great, and it can improve your application’s performance.
I have taken a conceptual approach to explain code splitting. If you want more technical grounds, please have a look at the official React docs. They do a decent job at explaining the concept technically.
Now you’ve got a decent checklist for tracking and fixing common performance issues in React apps. Go build some fast apps!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.