How to Use the React Profiler Component to Measure Render Performance
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
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.- Page is loaded and all emojis are displayed.
- User types the following characters one at a time, delayed by 300 milliseconds after each key press — “house”.
- Emoji results for “house” are displayed.
- After a 1,000 millisecond delay, the user presses backspace continuously, delayed by 300 milliseconds until the field is empty.
- All emojis are displayed again.
Debouncing with the Help of useEffect
and useRef
Hooks
useEffect
and useRef
HooksIn 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
!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.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
React.memo
and useMemo
In computing, or 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.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.
So what exactly did we accomplish here with
React.memo
and the useMemo
Hook?- By wrapping our
EmojisImages
component withReact.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
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.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 ✨.