This site runs best with JavaScript enabled.

React Production Performance Monitoring

Photo by Luke Chesser


How to set up performance monitoring for production React applications.

We should always ship fast experiences to our users, but sometimes something slips through our PR review process and our users start having a slow experience. Unless they complain to us, we often have no way of knowing that things are going so slow for them. User complaints is not a great policy for quality control.

Because we can't make every user install the React DevTools and profile the app for us as they interact with it, it would be nice if we could somehow track some of the render times and get that information sent to our servers for us to monitor.

There are existing solutions for monitoring and measuring the performance of your app regardless of what framework you're using (Lighthouse CI is especially interesting). That said, the React team has created an API specifically for measuring the performance of your React components in production. It doesn't give us quite as much information as the React DevTools do, but it does give us some useful information that will help you determine where performance issues lie.

Here's a basic usage example of React's <Profiler /> component:

1<App>
2 <Profiler id="Navigation" onRender={onRenderCallback}>
3 <Navigation />
4 </Profiler>
5 <Main />
6</App>

The onRenderCallback function is called with the following arguments:

1function onRenderCallback(
2 id, // the "id" prop of the Profiler tree that has just committed
3 phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
4 actualDuration, // time spent rendering the committed update
5 baseDuration, // estimated time to render the entire subtree without memoization
6 startTime, // when React began rendering this update
7 commitTime, // when React committed this update
8 interactions, // the Set of interactions belonging to this update
9) {
10 // Aggregate or log render timings...
11}

It's important to note that unless you build your app using react-dom/profiling and scheduler/tracing-profiling this component wont do anything. You can learn how to set that up from my blog post Profile a React App for Performance.

From here, you'll want to send the onRenderCallback data to a monitoring tool (like Grafana for example). Because re-renders can happen a LOT, I'd personally suggest batching them up and sending them together every 5 seconds or so. For example:

1let queue = []
2
3// sendProfileQueue every 5 seconds
4setInterval(sendProfileQueue, 5000)
5
6function onRenderCallback(
7 id,
8 phase,
9 actualDuration,
10 baseDuration,
11 startTime,
12 commitTime,
13 interactions,
14) {
15 queue.push({
16 id,
17 phase,
18 actualDuration,
19 baseDuration,
20 startTime,
21 commitTime,
22 interactions,
23 })
24}
25
26function sendProfileQueue() {
27 if (!queue.length) {
28 return Promise.resolve()
29 }
30 const queueToSend = [...queue]
31 queue = []
32 // here's where we'd actually make the server call to send the queueToSend
33 // data to our backend...
34 console.info('sending profile queue', queueToSend)
35 return Promise.resolve()
36}

Something to keep in mind is that because this is running in production, React does it's best to not hurt performance in the measuring of it (which is pretty sensible). Because of this, we're limited in the information we can receive. So you'll probably want to strategically place <Profiler /> components in your app with sensible id props so you can determine the source of the performance issue more easily.

Note also that you can nest these:

1<App>
2 <Profiler id="Navigation" onRender={onRenderCallback}>
3 <Navigation />
4 </Profiler>
5 <Profiler id="Main" onRender={onRenderCallback}>
6 <Main>
7 <LeftNav />
8 <Profiler id="Content" onRender={onRenderCallback}>
9 <Content />
10 </Profiler>
11 <RightNav />
12 </Main>
13 </Profiler>
14</App>

In this case, if <Content /> were to get a rerender, the Content and Main profiler onRenderCallbacks would get called (not the Navigation one). If <LeftNav /> got a rerender, then Main would get called, but not Content or Navigation.

Here's an example of what the data looks like:

1{
2 id: "Navigation",
3 phase: "update",
4 actualDuration: 0.09999994654208422,
5 baseDuration: 0.3799999540206045,
6 startTime: 104988.11499998556,
7 commitTime: 104988.45000000438,
8 interactions: [ // this is actually a Set, not an array
9 {
10 __count: 0
11 id: 3,
12 name: "menu click",
13 timestamp: 104978.33499999251,
14 }
15 ],
16}

You can learn more about that (experimental) interactions thing from this gist

Throwing this data into monitoring software could help you find some interesting trends and spot performance regressions (spikes).

Good luck! Happy profiling.

Discuss on TwitterEdit post on GitHub

Share article
Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Join the Newsletter



Kent C. Dodds