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.
NOTE: for any of this to work in production, you need to enable React's profiler build. There's a small performance cost for doing this, so facebook.com only serves the profiler build of their app to a subset of users. Learn more about how to enable the profiler build from Profile a React App for Performance.
Here's a basic usage example of React's
<Profiler />
component:
<App>
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>
<Main />
</App>
The onRenderCallback
function is called with the following arguments:
function onRenderCallback(
id, // the "id" prop of the Profiler tree that has just committed
phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
actualDuration, // time spent rendering the committed update
baseDuration, // estimated time to render the entire subtree without memoization
startTime, // when React began rendering this update
commitTime, // when React committed this update
interactions, // the Set of interactions belonging to this update
) {
// Aggregate or log render timings...
}
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:
let queue = []
// sendProfileQueue every 5 seconds
setInterval(sendProfileQueue, 5000)
function onRenderCallback(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions,
) {
queue.push({
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions,
})
}
function sendProfileQueue() {
if (!queue.length) {
return Promise.resolve()
}
const queueToSend = [...queue]
queue = []
// here's where we'd actually make the server call to send the queueToSend
// data to our backend...
console.info('sending profile queue', queueToSend)
return Promise.resolve()
}
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:
<App>
<Profiler id="Navigation" onRender={onRenderCallback}>
<Navigation />
</Profiler>
<Profiler id="Main" onRender={onRenderCallback}>
<Main>
<LeftNav />
<Profiler id="Content" onRender={onRenderCallback}>
<Content />
</Profiler>
<RightNav />
</Main>
</Profiler>
</App>
In this case, if <Content />
were to get a rerender, the Content
and Main
profiler onRenderCallback
s 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:
{
id: "Navigation",
phase: "update",
actualDuration: 0.09999994654208422,
baseDuration: 0.3799999540206045,
startTime: 104988.11499998556,
commitTime: 104988.45000000438,
interactions: [ // this is actually a Set, not an array
{
__count: 0
id: 3,
name: "menu click",
timestamp: 104978.33499999251,
}
],
}
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.