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.
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
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
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()Hook isn’t just for DOM refs. The “ref” object is a generic container whose
currentproperty 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.
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 🎉!
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.
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
- By wrapping our
React.memowe 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
getAllEmojisfunction 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 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
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.
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 ✨.
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.