How to Use the React Profiler Component to Measure Render Performance

Example Techniques to Improve Render Performance with Debouncing, Hooks and React.Profiler

Sep 9 · 6 min read

Let’s face it — core functionality powering React is complicated. Whether you’re trying to understand the reconciliation algorithm, component lifecycle, motivation of Hooks, or the newest holy grail… one thing many of these subjects have in common is a focus on rendering. In this post I detail by example some tools and techniques I’ve found helpful in improving rendering performance.

I put together an emoji finder app from Create React App, available on GitHub.

Example Scenario in Cypress

I used Cypress to mimic a user interaction case in the emoji finder app (emoji-finder). In the test browser, we can monitor the console log with Chrome DevTools. Let’s consider a scenario in which a user searches for a “house” emoji.

  1. Page is loaded and all emojis are displayed.
  2. User types the following characters one at a time, delayed by 300 milliseconds after each key press — “house”.
  3. Emoji results for “house” are displayed.
  4. After a 1,000 millisecond delay, the user presses backspace continuously, delayed by 300 milliseconds until the field is empty.
  5. All emojis are displayed again.
Example Emoji Search React App Without Optimization

Debouncing with the Help of useEffect and useRef Hooks

In our example a user types a term in the search field and our code does a lookup over 1,500 values. How often do we want to execute this lookup? The answer is about user experience. In the original state of our example app, we do a lookup whenever the search value changes. Since the UI is updated every time we do a lookup you can imagine how this can lead to a janky user experience from the expensive operation on the browser.

Debouncing “guarantees that a function is only executed a single time, either at the very beginning of a series of calls, or at the very end.” as explained in throttle-debounce. In our example, we define a threshold of 400 milliseconds to determine the end of a function call series.

Okay, fair enough — so, when does useEffect and useRef come into play? This goes back to the statement about how React is complicated. Because functional components are essentially render functions — a debounce function defined within them would be re-defined on every render and would therefore lose its debounce behavior. How do we overcome this? Behold… useRef!

Yes! The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. — Hooks FAQ

Debouncing adds asynchronous behavior because we’re subscribing to the occurrence of a final function call. How can we introduce this side-effect within a render function? You guessed it — useEffect 🏆! A detailed explanation of Hooks like useEffect can be found in React’s documentation.

Example component with debouncing, useRef and useEffect

Now, in our example app, with debouncing — when a user types in 300 millisecond increments (or any amount less than our 400 millisecond debouncing threshold); we don’t do a lookup until the final character input. In our example the user types “house”, we do a lookup, show results, the user then deletes one character at a time until the input is empty and then we do another lookup which returns all emojis we then display. Debouncing provides us a much less janky experience 🎉!

Memoization with React.memo and useMemo

In computing, memoization or memoisation is an optimization technique used primarily to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. — Wikipedia

Sound familiar? It makes me think of the component lifecycle method shoudComponentUpdate in which we tell React to only re-render a component if incoming props are different than props of the previous render. React.memo provides similar behavior to functional components that shouldComponentUpdate provides to class-based components.

In our emoji-finder app we can use React.memo so that we only re-render the emoji images when we have a different set of data. We accomplish this by creating an EmojiImages component and comparing an id of a data set. If the id changes — we re-render.

EmojiImages component with React.memo

The React Hook useMemo is not synonymous with React.memo, but it’s similar in behavior. We don’t use it to wrap an entire component but instead functionality within a component that can be memoized between renders.

In the example below, we return a memoized value every render after the initial. The empty array argument signifies that we have no dependencies for memoization.

const allEmojis = useMemo(() => getAllEmojis(emojiList.data), []);

Find the line above in the full example below.

useMemo being used by our Emojis component

So what exactly did we accomplish here with React.memo and the useMemo Hook?

  • By wrapping our EmojisImages component with React.memo we ensure it only re-renders when we have a new set of images. In our test case this will only occur initially when showing all emojis, after the user has completed typing of the word “house” to show house emojis, and when the user has completed deleting all the characters (because we’ll need to show all images again).
  • We need to have access to a list of all emojis to show when the search is empty or whenever a user enters a search that yields no results. By memoizing the getAllEmojis function we essentially cache this value across all renders — a list of all emojis to show when needed. In our test case this memoization is at play when the user deletes all characters in the search and all emojis are displayed, because we have this value as a cached result of the memoized function.

Measuring Render Performance with React.Profiler

React.Profiler is an interesting addition to React in version 16.9 which offers a programmatic way of gathering render performance measurements (similar to the React Profiler for DevTools as announced in the blog). Among some interesting metrics provided by the onRender callback function — I’ve found actualDuration to be the most useful. You can see how it’s being used in our example app in App.js and profiler.js. I collect the sum of actualDuration during all renders in a value named cumulativeDuration and utilize the nifty console.table method to produce a log in DevTools.

Results from React.Profiler utilizing console.table

Conclusion

With its volume of continuous growth — React can be complicated at times, but on the flip-side offers a robust number of backwards-compatible, standalone features to help us optimize our applications.

By utilizing Hooks for memoizing across renders and React.Profiler to measure impact, we have a robust tool belt to ensure our component rendering is performant.

With all the changes above we improved the total render duration of our test case from 937.56 milliseconds to 484.86 milliseconds ✨.

Example Emoji Search React App With Optimization

Credits

Adam Henson, the author of this post is the founder of Foo — a website performance monitoring tool. Create a free account with standard performance testing. Automatic website performance testing, uptime checks, charts showing performance metrics by day, month, and year. Foo also provides real time notifications when performance and uptime notifications when changes are detected. Users can integrate email, Slack and PagerDuty notifications.

Adam Henson

Written by

Software engineer in NYC

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade