Boosting Your React App Performance with React Profiler

Post Editor

A guide to measuring and improving React Apps performance with React Profiler. Accompanied by a web app that makes it easy to follow along.

11 min read
0 comments
post

Boosting Your React App Performance with React Profiler

A guide to measuring and improving React Apps performance with React Profiler. Accompanied by a web app that makes it easy to follow along.

post
post
11 min read
0 comments
0 comments

Writing a React application is easy.
Writing a good React application is more complicated.
Writing a good React application that also works fast, well, this takes a lot more than just programming skills.

Even if you’re a great programmer, sharp as a tack, smart as a whip, as good with React as Dan Abramov — you miss stuff; it happens. And sometimes it’s not trivial at all to find out what it is that you’ve missed.

Today we’re going to talk about the ultimate tool for tackling React performance issues — React Profiler.

Background
Link to this section

First introduced in 2018 React Profiler has been a part of React Dev Tools Chrome extension for a while. You’d expect such a powerful tool to gain a lot of popularity over the years, but I keep seeing people in the professional community using console.log for counting the number of renders and measuring rendering time.

Whether it is because people are unfamiliar with the Profiler, or because it seems too complicated to them — we’re going to tackle both; you’re going to learn what it is and how to use it and you’ll see it’s not complicated at all.

So let’s jump right into it.

Lab rat
Link to this section

To showcase React Profiler, we’ll have a very simple application with an auto-generated list of numbers that can be filtered by a search term we’ll enter in a text box.

Here we go:

<>Copy
const items = Array.from({ length: 200 }, () => `${chance.integer()}`); export const FilterableList = () => { const [searchTerm, setSearchTerm] = useState(''); return <div className={'filterableList'}> <Filter onValueUpdated={setSearchTerm} /> <List entries={items.filter(item => item.includes(searchTerm))} /> </div> }
<>Copy
export interface ListProps { entries: string[]; } export const List: FC<ListProps> = ({entries}) => { return ( <div className="list"> {entries.map((value, index) => <ListItem key={index} value={value}/> )} </div> ); } export const ListItem: FC<ListItemProps> = ({value}) => <div className={'item'}>{value}</div>

Nothing complicated here they said, self-explanatory code they said.

To make it easier to follow I made this app available online here. You can enter the site, open the Debug Tools and follow along with this article.

The full code of the app can be found on Github.

React Profiler
Link to this section

Now that we have our app up and running we can meet the React Profiler. I assume by now you already have the React Dev Tools extension installed but if not please head to the Chrome Store and do yourself a favor.

Once installed, React Dev Tools will be enabled on any website built with React.

Go to our web app and open Chrome Dev Tools. You’ll notice that one of the tabs will be Profiler:

Content imageContent image

Profiling doesn’t work on the fly — first, you have to record a profiling session and then you can analyze it.

Before we start recording we need to enable one important setting in React Dev Tools settings:

Content imageContent image

Click on the gears icon and check the Record why each component rendered while profiling checkbox:

Content imageContent image

The second option (Hide commits below) is also useful, particularly when you have lots of commits and want to filter the insignificant ones (those that are below a certain threshold).

Recording a profile
Link to this section

To start recording a profile click on the blue Record button:

Content imageContent image

Alternatively, you can reload the page and start the recording immediately:

Content imageContent image

After the recording started, play with your app a bit or reproduce a particularly problematic scenario and then stop the recording:

Content imageContent image

For our test app I’ll just enter 111 in the text field and then delete the digits one by one (111 -> 11 -> 1 -> ‘’).

After stopping the recording this is what we get:

Content imageContent image

Now let’s see what it means.

Profiler UI
Link to this section

The Profiler UI can be logically separated into 4 main sections:

Content imageContent image
  1. Chart selection — allows choosing between two different representations of your app profile — Flame Chart and Ranked Chart. We’ll cover both in detail.
  2. Chart area — a graphical representation of a single commit in your application profile.
  3. Commits — each bar represents a single occurrence of the commit phase in the lifecycle of your application. Whenever you select a commit by clicking on it, the chart area and the commit information are updated accordingly.
  4. Information panel — the details about a single selected commit phase or a single selected component.

Now let’s talk about it in detail.

Commits
Link to this section

React reconciliation algorithm is split into two phases: render and commit.

  • The render phase determines what changes need to be made to e.g. the DOM. During this phase, React calls render and then compares the result to the previous render (the diffing algorithm).
  • The commit phase is when React applies any changes. (In the case of React DOM, this is when React inserts, updates, and removes DOM nodes.)

Here is the phases diagram for classic React components (by Dan Abramov) and here is a similar diagram for hooks (by Guy Margalit).

As previously mentioned, each bar in the commits section represents a single commit — the taller the bar the longer the commit took to render. The commits are also distinguishable by a greenish-to-yellowish color gradient — yellows are the less performant ones and greens are more performant.

Thus, a taller yellow bar represents a commit that took longer than a shorter green bar.

The currently selected commit is colored blue.

Charts: Flamegraph Chart
Link to this section

The flamegraph chart view represents the rendering tree of your application for a specific commit. Each bar in the chart represents a React component. The components are organized from the rendering root to the leaves (root is the topmost component and leaves are the bottommost).

Content imageContent image

As you can see, Header and FilterableList are App’s children so they appear next to each other and below the App component.

The width of the bar represents how long the component and its children took to render. The color of the bar represents how long the component itself took to render (greenish is fast, yellowish is slow).

Thus, in the example above the width of FilterableList represents the time that took FilterableList to render including the time it took List to render.

On the other hand, you can see that FilterableList is green and List is yellow and it correlates with the numbers — it took FilterableList only 0.5ms to render and it took List 1.6ms to render.

But what happens if a component is not rendered at all during a particular commit?

Let’s take a look at the 4th commit:

Content imageContent image

The App and Header components don’t change upon filtering, so they are rendered only once — during the first commit. On the following commits, both components are greyed out, however, they still look a bit different. So what’s the difference?

Grey fill — a component that did not render during this commit but is part of the rendering path (e.g. App didn’t render but it is a parent of FilterableList which did render).

Grey gradient stripes — a component that did not render during this commit and also is not part of the rendering path (e.g. Header didn’t render but it also doesn’t have any children that did render)

Also, you might have noticed that the App component bar still has a width although it didn’t render.

So let’s refine the definition a bit.

The width of a bar represents how much time was spent when the component was last rendered and the color represents how much time was spent as part of the current commit.

Last but not least, you can zoom in or out on a chart by clicking on a component.

Zoomed out:

Content imageContent image

Zoomed in:

Content imageContent image

Zoomed out:

Content imageContent image

Charts: Ranked Chart
Link to this section

Similar to a flamegraph chart, a ranked chart represents a single commit. However, unlike in a flamegraph chart, the components are ordered by rendering time and not by rendering order.

That means that components that took the longest to render are at the top.

Another difference is that the component’s bar width represents the time that it took the component to render not including its children. This means there is a direct correlation between the color and the width.

Content imageContent image

As you can see, List took the longest to render so it is located at the top, it is the widest among the bars and it is the yellowest among the bars.

Components that didn’t render during this commit won’t appear in the ranked chart.

Similar to the flamegraph chart zoom in and out is possible by clicking on a component.

Content imageContent image

Information panel
Link to this section

The information panel has two different applications.

1. Selected commit

Content imageContent image

When no component is selected (zoomed in) it shows an overview of a commit that is currently selected in the commits section. The data includes the time (since the app start) it was committed at, the time it took to render, and the priority.

2. Selected component

Content imageContent image

When you click on a component (zoom into it) in one of the chart views, the information panel will show the details about this component. This includes why the component has rendered during this particular commit (if you enabled this option in settings) and the list of commits with timestamps. The list is interactive and allows you easily navigate between different commits in which this particular component has been involved.

Taking the lab rat to the next level
Link to this section

Now that we’re acquainted with React Profiler let’s see how we apply this knowledge to a real-life scenario.

Let’s take another look at our app.

<>Copy
const items = Array.from({ length: 200 }, () => `${chance.integer()}`); export const FilterableList = () => { const [searchTerm, setSearchTerm] = useState(''); return <div className={'filterableList'}> <Filter onValueUpdated={setSearchTerm} /> <List entries={items.filter(item => item.includes(searchTerm))} /> </div> }
<>Copy
export interface ListProps { entries: string[]; } export const List: FC<ListProps> = ({entries}) => { return ( <div className="list"> {entries.map((value, index) => <ListItem key={index} value={value}/> )} </div> ); } export const ListItem: FC<ListItemProps> = ({value}) => <div className={'item'}>{value}</div>

The logic inside the components is pretty straightforward, so it will be hard to improve.

Instead, we’ll focus on rendering performance to try and reduce the number of renders. Since all we’re doing between the commits is filtering, we’d assume that the items are rendered once and then just removed from the DOM when a filter is applied. This means list items shouldn’t be rendered twice as we filter. However, this is not happening. If you look at the lab rat profile and switch between the commits in the commits panel, you’ll notice that the list items are rendered upon every commit. Why is this happening?

Let’s zoom into one of the items in the second commit and try to figure it out.

Content imageContent image

Zooming in provides us with helpful information — the item has been re-rendered since its value prop has changed (see the Information Panel).

Why would the value change? Well, every time we filter the list a new array is created. Since we’re using item index as a key for ListItem components, the distribution of list values among the components will be different every time we change the filter value.

For example, at the first render, the first entry in the array was rendered using a component with key=1. However, on the second render when we filtered out a few values from the array, the first entry might be different. React will reuse the component with key=1 from the first render, but the value has changed because the first entry has changed, hence re-render.

To fix this behavior we’ll assign an ID to every entry in the array when we first create it and use it as a key for the item instead of using item index.

Let’s apply the fix and see what’s changed (the updated app is available here):

<>Copy
const items = Array.from({ length: 200 }, (_, index) => ({ value: `${chance.integer()}`, id: index})); export const FilterableList = () => { const [searchTerm, setSearchTerm] = useState(''); return <div className={'filterableList'}> <Filter onValueUpdated={setSearchTerm} /> <List entries={items.filter(item => item.value.includes(searchTerm))} /> </div> }
<>Copy
export interface ListProps { entries: {value: string, id: number}[]; } export const List: FC<ListProps> = ({entries}) => { return ( <div className="list"> {entries.map(({id, value}) => <ListItem key={id} value={value}/> )} </div> ); } export const ListItem: FC<ListItemProps> = ({value}) => <div className={'item'}>{value}</div>

Surprisingly it changed nothing — the numbers are the same and the item components are still re-rendered upon every commit. Let’s take a closer look:

Content imageContent image

As you can see there is one thing that did change — the reason for a re-render. Now the ListItems are re-rendered because their parent component (List) is re-rendered, even though nothing changes for these components — neither the ID nor the value.

Luckily we know how to solve these kinds of issues — React.memo!
Let’s put memo on the list items and see how much it improves:

<>Copy
export const ListItem: FC<ListItemProps> = memo(({value}) => <div className={'item'}>{value}</div>)

Here we go!

Content imageContent image

None of the previously rendered items are re-rendered on the following commits. And look at the render duration — we cut it by the factor of 2!

Finishing words
Link to this section

The ultimate solution to this performance problem would be using a list with a virtual scroll which would reuse the same items for different data and thus save on re-mounting the components.
However, the goal of this article is to learn how to use the profiler and not to provide the best solution for the Lab Rat app. So I hope this goal is achieved and you understand how it works and what it is capable of.

Feel free to play around with the Lab Rat live application or fork the Github repo and play with it locally:

Important note: React DOM automatically supports profiling in development mode for v16.5>, but since profiling adds some small additional overhead it is opt-in for production mode. This gist explains how to opt-in.

This is it for today, follow me if you liked the article, comment here, or DM me on Twitter if you have any questions.

Cheers!

This and other articles are available for free on my personal blog. Make sure to sign up to get the latest and greatest!

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Fullstack engineer and guild master @ Wix.com, author of @angular-builders and jest-marbles, drummer and kitesurfer.

author_image

About the author

JeB

Fullstack engineer and guild master @ Wix.com, author of @angular-builders and jest-marbles, drummer and kitesurfer.

About the author

author_image

Fullstack engineer and guild master @ Wix.com, author of @angular-builders and jest-marbles, drummer and kitesurfer.

Looking for a JS job?
Job logo
Senior React Native Developer / OwnRock

Wildix

Ukraine
Remote
$42k - $72k
Job logo
Senior Full-stack (React+ node) developer

Intetics

Ukraine
Remote
$60k - $108k
Job logo
React Developer

Digital Scientists

United States
Remote
$105k - $125k
Job logo
React Native Developer (Team Lead)

Visible Magic

Ukraine
Remote
$60k - $96k
More jobs

Featured articles