Debugging Performance Issues in React
7 min read
I’m as guilty as the next dev at just relying on
console.log to figure out issues and bugs in my React app. This doesn’t really cut it though, when it comes to debugging performance issues you might be noticing in your React app. Thankfully, React’s developer tools have really stepped up its game in recent years, and there’s some great tooling available to help with this.
How do you even know if you have performance problems in your React app?
React tends to be blazingly fast out of the gates, so I don’t generally try to optimise performance if nothing appears to be wrong. The main triggers for needing to do performance investigations or debugging tends to be:
- QA testers complaining that it “feels” janky or slow i.e. some sort of human perception of slowness.
- Poor Chrome Lighthouse scores, particularly in the “performance” section. For a blog post on Lighthouse, check out Improving Lighthouse Scores For bionicjulia.com.
- If your page is an important landing page, you might also have a benchmark time for which you want your page to load under, to try to minimise bounce rates and increase conversion rates.
- Users might also complain about not being able to access your page, or your page being too slow to load. Hopefully it doesn’t get to this point though. 😓
How to diagnose React performance issues?
The main tools I use are:
- The Chrome Performance tab (and Network tab, for identifying suboptimal network calls).
- The Chrome React Devtools extension, which gives you access to Components and Profiler tabs.
1. Chrome developer tools
Chrome comes with a wealth of amazing developer tools. If you’ve never explored the tools available, I’d highly recommend setting aside some time to try playing around with them. Also, check out my previous blog post on Chrome devtools for a TL;DR.
For the purposes of performance, the main tabs I tend to use are “Network” and “Performance”.
- Clicking on each individual row gives you further details on the time it took for each resource to load.
The Performance tab comes next. It’s super useful, but can be very overwhelming if you’re not sure where to look.
First thing - if you want to put your app through its paces, slow down the processing power of the testing environment.
Next, click the record button and play around your app as a user would. This is the circle icon on the far left. Stop recording when you’ve taken the actions you want - I’d recommend not recording for longer than you need as you’ll be absolutely drowning in data otherwise, making it tougher to do the analysis.
What you’ll see next is a flame graph, which maps out the various actions being run over time. The red bar at the top highlights steps that took the longest time to load, so those are good areas to start. I also like looking at the timings section to see when events like “First Paint” happens.
Clicking on each row here gives you more details in a separate menu at the bottom. Click on the “bottom up” tab and sort by time to see your worst offenders. (I have nothing much to show in my case, because I use NextJS and this page is statically generated at build time. 😉)
2. Chrome React Devtools
If you’ve been developing React web apps, and have not yet used the Chrome React Devtools, you’re missing a treat. Go and download it now - I’ll wait here.
To test it out, go to a website that runs React (you’ll know it’s React if the extension icon is highlighted - click on it). You should see 2 new tabs in your Chrome devtools - Components and Profiler.
Starting with Components, the setup I like to have is to:
- Enable the highlighting of updates when components render; and
- Enable the recording of why each component rendered.
Note: to get this menu, click on the cog icon in the tab.
What this does is to highlight when components re-render because of user interactions on the app. This will thus highlight to us when unexpected component re-renders might be happening, which may help us identify components that need better structuring or refactoring to limit re-renders. Here’s an example of what the render highlighting looks like (see the green boxes?).
The Component tab also displays the React app component tree and allows you to click into each individual component and view its props and state. The reason the names of the components are single letters here is because code has been minified for production. If I click on my “search bar” component, what’s shown on the right hand side are the props and React hooks this component has. If you’re in development mode, you will be able to edit the state and props and have that sync up with what’s shown in the UI.
The other tab that’s included is the Profiler tab. This can only be run in development mode. It acts quite similarly to the Performance tab in Chrome Devtools, but this one is very specific to React components.
If you click on the “record” icon, take a few actions then stop recording again, you’ll see something similar to the below. In my example, I made a search query.
The mini bar-chart on the top right shows the series of commits, which are essentially React component renders. Green bars indicates components rendered fairly quickly. Yellow and red bars are the ones to look out for. Grey bars indicates no (re-)renders. Clicking on a bar makes it blue and highlights the component and provides more details on it. In this case, the Search component took a relatively longer time to re-render, because its state change (i.e. the search term - “hook 4”) was resulting in new results having to be displayed. I can’t stress enough how helpful the “Why did this render?” section is. 🤩
Clicking on any of the times under the “Rendered at” section will display tiles on the left hand side, of all the components rendered at that same time.
How to fix React performance issues?
This will clearly be dependent on what you actually find during your debugging exercise, but typical culprits will be:
- Unnecessary component re-renders
- Slow API calls
- Large, suboptimal image sizes
- Memory leaks
Some ways of improving the performance of your React app might include:
- Using React.memo (React PureComponent), useMemo and useCallback hooks to minimise component re-renders. Read my in-depth blog posts on React.memo and React.useMemo for more info.
- Minimising function calls (especially network requests) by using debounce or throttle, and batching network calls when possible.
- With images, try to use the optimal image size required for the screen sizes you’re targeting.
- Minimising memory leaks, by making sure you clean up your event handlers, timeouts and intervals when components unmount.
- Use the “Memory” tab in Chrome Devtools to filter for “detached” objects to identify these.