React is a library for creating front end views. It has a big ecosystem of libraries that work with it. Also, we can use it to enhance existing apps.
In this article, we’ll look at how to profile a React app’s performance with the Profiler API.
Profiler API
The Profiler
component measures how often a React app renders and what the cost of rendering is.
It helps identify parts of an app that are slow and may benefit from optimizations like memoization.
This component adds overhead, so it’s disabled in the production build.
Basic Usage
We can use it to measure performance as follows:
import React, { Profiler } from "react";
import ReactDOM from "react-dom";
import { unstable_trace as trace } from "scheduler/tracing";
class App extends React.Component {
constructor(props) {
super(props);
this.state = { msg: "" };
}
onRender(
id,
phase,
actualDuration,
baseDuration,
startTime,
commitTime,
interactions
) {
const performanceData = [
`id: ${id}`,
`phase: ${phase}`,
`actualDuration: ${actualDuration}`,
`baseDuration: ${baseDuration}`,
`startTime: ${startTime}`,
`commitTime: ${commitTime}`,
`interactions: ${JSON.stringify([...interactions])}`
].join("\n");
console.log(performanceData);
}
componentDidMount() {
trace("initial render", performance.now(), () => {
this.setState({ msg: "foo" });
});
}
render() {
return (
<Profiler id="app" onRender={this.onRender.bind(this)}>
<p>{this.state.msg}</p>
</Profiler>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In the code above, we have the Profiler
component wrapped around our main app code in the rendr
method.
We add an id
prop so that we can identify what part of the React tree was committed if we’re using multiple profilers.
Also, we have an onRender
prop which is set the our onRender
method that we defined.
onRender
is a callback that has the following parameters:
id
— a string that we set to identify the part of the React component tree that we just committed.phase
— a string that can either have the value'mout'
or'update'
. This identifies whether the tree has just been mounted for the first time or re-rendered due to a change in props, state or hooks.actualDuration
— a number that shows the time spent rendering theProfiler
and its descendants for the current update. This indicates how well the subtree makes use of memoization (e.g. withReact.memo
,useMemo
,shouldComponentUpdate
). This should decrease significant after the initial mount since child components will only be re-rendered if props changebaseDuration
— a number that indicates the duration of the most recentrender
time of each individual component within theProfiler
tree. This estimates a worst-case cost of renderingstartTime
— a timestamp when React began rendering the current updatecommitTime
— a timestamp when React committed to the current update. It’s shared between all profilers in a commit, enabling them to be groupedinteractions
— aSet
of interactions that were being traced whenrender
orsetState
were called.
In the onRender
method, we logged all the parameter values, and we get the following when App
renders:
id: app
phase: update
actualDuration: 0.38499990478157997
baseDuration: 0.045000109821558
startTime: 908.5849998518825
commitTime: 909.2250000685453
interactions: [{"_count":1,"id":0,"name":"initial render","timestamp":906.7250001244247}]
The interactions
output is from the trace
method call. It’s tracking the time when the code inside the callback we passed as the last argument of trace
we run.
This lets us associate the performance information with the events that caused the app to render.
From the output above, we can see that the render was caused by an update, and it’s because of the initial render
interaction that we have in the componentDidMount
lifecycle hook.
In the trace
function call, we have the string with the name that we set to identify the interaction, performance.now()
is a more precise version of Date.now()
to get the current date-time which we pass in as the timestamp to start the trace.
The 3rd argument is a callback with the code that we want to trace the interaction for.
Conclusion
We can use the Profiler
component to add performance tracking capabilities when we’re developing the app.
It’s useful because we can use it to see what parts of the React app is performing slowly and can benefit from caching or memoization.
To tracking interactions, i.e., the events that caused an update to be scheduled, we can use the unstable_trace
function from the scheduler/tracing
package to trace the timing of the interactions.