In this article, I will introduce a few different ways to optimize the performance for React apps. And use an example to demonstrate how to choose a proper solution when doing the optimization.
Before we start, let's look at an example.
Here we have 24 checkboxes to allow the users to select the time they want.
The problem of this example is that every time when the user clicks the checkbox, all the checkboxes will also re-render.
So, how can we solve this problem?
React DevTools Profiler
Before we start optimizing our application, we need to know how to identify the performance issues of our application?
react-dom 16.5+ and react-native 0.57+ provide enhanced profiling with the React DevTools Profiler.
Using React DevTools Profiler is simple, click the record button at the top-left corner, interact with our application and stop the record by clicking the same button again. Then we ended up having the result to identify the issues.
When we check the flamegraph through React DevTools Profiler, we can see the un-necessary re-render.
Now we know the problem, let's try a few different solutions for that.
PureComponent
First, we can try the simplest solution - PureComponent, we just need to change our class extend from component to PureComponent, then React will do the rest for us.
// before
export default class CheckBox extends React.Component {
...
}
// after
export default class CheckBox extends React.PureComponent {
...
}
But after we change to PureComponent
, we can see it didn't prevent the unnecessary re-render. The reason is that we create a new handleToggle function every time. So even we apply PureComponent
it still re-render all the CheckBox components when App component re-render.
ShouldComponentUpdate
Because PureComponent
didn't work. So now we have to do the check on our own. We can use ShouldComponentUpdate to block un-necessary render.
shouldComponentUpdate(nextProps) {
const {value, isChecked} = nextProps;
return this.props.value !== value || this.props.isChecked !== isChecked
}
Now, when we check React DevTools Profiler again, we will see only the click checkbox will re-render.
React.memo
If we want to use function components rather then class, we have another option - React.memo.
React.memo
will do the same check as PureComponent
. But it allows us to pass the second parameter to do the custom check similar with ShouldComponentUpdate
. But we need to notice that the return value should be opposite with ShouldComponentUpdate
.
export default React.memo(CheckBox, (prevProps, nextProps) => {
return prevProps.value === nextProps.value && prevProps.isChecked === nextProps.isChecked
});
useMemo
Another solution for function components is to use hooks - useMemo.
export default function CheckBox ({value, isChecked, handleToggle}){
return React.useMemo(() => {
return (
<div>
<label>
<input type="checkbox" value={value} checked={isChecked} onChange={handleToggle} />
{value}
</label>
</div>
)
}, [value, isChecked]);
}
Although this can help us prevent un-necessary re-render. We will see an error from eslint-plugin-react-hooks.
The eslint
will remind us to add handleToggle
into our dependency array. But we can't because that's something we have to ignore to prevent un-necessary re-render. We can easily use eslint-disable
to prevent this error. But actually, this error message does point out an important problem.
Although most of the above solutions (except PureComponent
) can help us optimize the performance. But those custom logic also make the code harder to maintain and might bring some potential bugs.
Let's say when another team member adds a new prop - isDarkMode
for the Checkbox component, if he or she forgets to adjust the custom logic in ShouldComponentUpdate
or React.memo
, then the dark mode won't work because Checkbox will not re-render when isDarkMode
prop change.
So, how can we solve this problem?
useCallback
A better way to solve this performance problem is to prevent creating a new handleToggle
function every time.
We can change our App component into a class component. Or use another hook - useCallback to do the job.
const handleToggle = useCallback(targetTime => {
setTimeCheckboxes(timeCheckBoxes => {
return timeCheckBoxes.map(({ time, isChecked }) => ({
time,
isChecked: targetTime === time ? !isChecked : isChecked
}));
});
}, []);
Due to we won't create a new toggle
function every time now. We just need to apply PureComponent
on Checkbox. Then we can prevent the un-necessary re-render without adding any custom logic into our codebase.
Measurement
Furthermore, rather than just understanding how to optimize our application, we also need to know how to measure the performance of our application.
React Profiler
React provide a component to help us achieve that - Profiler.
Just simply wrap our App component with Profiler
, then we can get the information we need.
<Profiler id="app" onRender={onRenderCallback}>
<div className="App">
...
</div>
</Profiler>
onRender
prop will pass those information into our callback function. So we can print the information we need.
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...
}
Now that we can know the difference before and after the optimization.
Chrome DevTools: Performance
Another option is to use Chrome dev-tools. We can select Performance
tab and start record just like what we did on React DevTools Profiler.
(here I slow down the CPU to make it easier to identify the performance problem, we can also simulate a slower network if we need)
Then we can see the result like this.
- Before: 152.72ms, 132.22ms, 204.83ms
- After (apply
useCallback
andPureComponent
): 15.64ms, 18.10ms, 12.32ms
Conclusion
React provide many APIs and tools to help us optimize our application. While we try to optimize the performance, we need to choose the solution wisely, this will make sure our code is still easy to maintain after we improve the performance of our application.
--
Top comments (2)
A shame this post is getting so little attention :(
Dude, this is actually so helpful, thanks !