Some time back, I built Gauge Generator as a weekend project. This app helps to create custom gauges and export them as SVGs to be used in a dashboard design projects. It was built using React framework and create-react-app tool.
Since it was a fun little weekend project, I did not spend much time on optimizing it. As the app now complete, I thought it's a good time to take another look at its performance and optimize it if necessary.
Measuring React app performance
"If you can't measure it, you can't improve it". So, we need to first see where the issue is in order to fix it.
Measuring React app performace is not difficult. React uses User timing API to log performance metrics which we can easily look into via browser's developer tools.
Additionally, we can use React Developer tools Higlight Updates feature to visually see which React components are rendered as the app state changes thus figuring out which components are rendered unnecessarily.
1. Finding wasted renders
First enable "Highlight Updates" in React Developer tools preference. Now interact with your app and you will see the components are highlighted.
In the above recording, you can see lots of boxes displayed around components as I increase the slider value. You can also notice that even though I only make change in one React component, it triggers updates in all other react components as well. Ideally, as I change Gauge value, I want only the Gauge and the Gauge value component to get updated, not the whole app.
2. Measuring wasted renders
Now we know that our app renders components unnecessarily, lets measure how much CPU cycles are getting wasted.
Open Chrome Dev tool's Performance tab and record the app as you increase the slider value. But before doing this, disable any React addons in the browser, as they may impact our measurements. Record the measurements for less than 10 seconds, else the Chrome might hang. For our case, recording for 3 seconds is more than enough. After the recording stopped, Chrome will open the detailed report (click to enlarge).
When you focus on one single render cycle, we can see that the component InputColor
and InputField
are getting updated and taking almost 90ms for each render cycle. This render is unnecessary and can be optimized.
Optimizing the React app performance
Now we know where the bottleneck is. We need to avoid rendering components unless it is really needed. In our case, the problem lies on these two components - InputColor
and InputField
. We can go about fixing these components in two ways.
- Using
shouldComponentUpdate
method, you can determine if a component needs to be updated/rendered as its state/prop changes. You can manually compare React component's next state/props with currect state/props and return eithertrue
orfalse
to let React know to update or not. - Or, by Using
PureComponent
, you can let React to do the shallow comparison of state/props values. This is a recommended approach as it saves us from doing manual comparison.
So, I updated both InputColor
and InputFields
components to extend from PureComponent
instead of Component
.
import React, { PureComponent } from "react";
class InputColor extends PureComponent {
/* code here */
}
class InputField extends PureComponent {
/* code here */
}
Below is how the application rendered after making the above change. If you see, now there are no unnecessary renders as I slide the Gauge value. Only the top level components are re-rendered.
Perfect! With a simple fix, we were able to reduce the number of rendered components to a minumum. To confirm the improvment, lets measure the metrics and see (click to enlarge).
If you compare the above results with the previous measurement, you can clearely see we have no calls to InputColor
or InputField
is made, thus saving around 90ms for each render cycle.
Conclusion
In most cases, switching to use PureComponent
or manually defining shouldComponentUpdate
will always result in performance improvment. But you need to make sure that when a state changes, you don't mutate the object, but instead you always return a new object. This is because the PureComponent
does a shallow comparision, so if you mutate a nested property, it won't be able to detect the change, hence it won't re-render your component. So, just be sure not to mutate state.
Apart from using PureComponent
, breaking large component into small components and passing the state as props to them will also help in saving wasted renders. Just make sure you only pass the props that is specifically needed by the small components. This way, PureComponent
will get to compare the prop change and avoid rendering if necessary.
undefined