Three Practical Examples of Concurrent React
Optimizing Performance using React 18 startTransition API
The React team recently announced the plan for React 18, bringing a lot of cool new features like automatic batching, new suspense SSR Architecture, and new APIs that leverage concurrent rendering like the startTransition
API.
For the past few years, the React team has been working on major features that will support concurrent rendering, those features are going to have a remarkable effect on both the user experience and the development process.
There are a lot of resources where you can learn more about concurrent React, so I’ll not cover how it works behind the scenes in depth. Instead, I’ll show you three practical use-cases where I leveraged the new React startTransition
API to resolve major performance issues.
Disclaimer:
At the time of writing this post, React 18 is still in Alpha. You are more than welcome to explore it and play with it, but it’s not yet stable and ready for production usage.
In addition, all of the examples in this post are presented using Codesandbox which has a performance overhead because it runs in development mode. For the most realistic results, you can run the examples in this repo in production mode.
A few words about the React startTransition API
The new React startTransition
API will help make your application more responsive, even during large screen updates. With this new API, you can substantially improve user interactions by marking specific updates as “transitions.” React will let you provide visual feedback during a state transition and keep the browser responsive while the transition is in progress.
Use case#1 — Searching for Pokemon
I love Pokemon.
That’s why I’ve decided to build a small screen where I can search for all the Pokemon in the world and get some details about them. There are a lot of Pokemon out there and I didn’t want to start messing with DOM virtualization techniques. I decided I just “Gotta render em all.”
That list happened to be quite big, and the search input felt a bit laggy.
Let me give you a live demonstration:
If you are working on the average computer, you probably noticed the delay while typing. If you didn’t, let me help you get off your high horse with the following trick ;)
- Open the developer console
- Click performance
- Change CPU throttling to 4x slowdown
Now for sure, you’ll notice the delay. This is not the optimal user experience.
Input typing is a sensitive UX action. The user is focused on the input while typing in it, and is expecting to get very quick feedback from the app that whatever is typed on the keyboard is displayed in that input.
Think about yourself as a user: isn’t it frustrating that you type something and don’t see those letters being painted until after a half-second? It immediately makes you think, “Argh, this app is so slow!”
It’s very important to render those UX-sensitive actions as fast as we can, and give quick feedback to the user.
Now, let’s have a look at the code:
Why is the search so laggy?
The reason for the lagginess is because we render A LOT of pokemons on each keypress. We have around 1300 pokemons at the initial render. Let’s assume we press ‘b’; after pressing this key, React recalculates all the changes against the old DOM representation, and commits those changes to the DOM accordingly. If it’s dealing with around 1300 elements, rendering takes time.
While React renders the new state to the DOM, interaction with the UI is completely blocked. You can’t do anything, can’t click the buttons, can’t see the “balbasuer” that you’ve just typed. Only after it finishes the rendering does the UI become fully responsive.
What can we do about it?
We all know that it’s a common standard to “throttle” the input’s onChange calls. That makes it call only after a specified maximum frequency, but as stated in this wonderful doc from React:
on lower-powered devices, we’d still end up with a stutter. Both debouncing and throttling create a suboptimal user experience.
And this is where React’s new startTransition
API comes into the picture.
With concurrent React, the browser can work on several state updates concurrently.
By using the startTransition
API, we can tell React:
“Hey React, listen, I know you want to update this list, but right now, rendering whatever the user is typing is way more important than updating that list. PLEASE stop rendering the list for a while and focus on rendering the new key the user has just pressed! You can come back to the list later.”
It’s all about prioritization.
Leveraging concurrent React for our example
We will help React mark what is less important to render, and thus help React prioritize things.
We will make the following changes:
- Use the new
ReactDOM.createRoot
instead ofReactDOM.render
, this is part of the upgrading process to React 18 and it will prevent console warnings. - Split the
text
state into two different states, one for the input and one for the query:text
andsearchQuery
. - We will use the
useTransition
hook that will provide us access to two important variables:startTransition
andisPending
.startTransition
is a function that we will use to tell React which renders are “less important.”isPending
is a boolean that tells us whether our list is pending to be rendered or not. - We will wrap the
setSearchQuery
statement withstartTransition
. - We will pass to
PokemonsCard
thesearchQuery
state instead of thetext
state. - We need to wrap our pokemon list with a
React.memo
. (more on that here) Since React renders all the children of a parent,setText(e.target.value)
will cause a rerender of the list on each keypress, so we want the list to be controlled only by thesearchQuery
state. - We will use the
isPending
state to show a nice, elegant loader near the search input that shows that the list is waiting to be rendered.
Switch to ReactDOM.createRoot
in our index.js file:
And our modified components:
Drum roll please… And the result
Magical, isn’t it?
Now React knows that rendering the user input is more important than rendering that list, and the user experience feels a lot more smooth and natural.
From here on I’ll speed things a bit up: I’ll just show “before” and “after” examples. There is no need to dig into details since the principle is the same among all of the examples. You are more than welcome to open the code and drill into things :)
Use case#2 — Apple stock
I like Apple, and also the stock market.
That’s why I’ve decided to build a small screen where I can view Apple’s stock over the years, watching for different trends. I’ve used the wonderful BrushChart from AirBnb’s great visualization library: visx.
* Notice that there’s a list below the chart that gets filtered according to the chart.
Play with that a bit.
Change the CPU throttling and play with that again.
Do you notice how the slider stutters?
This is the code:
The problem here is much the same as the pokemon search issue. We change the slider, which causes a rerender of a huge amount of data, which blocks further user slider changes to be painted.
Let’s apply a similar change here as well. We will wrap the setStock
statement with the startTransition
function:
The result:
Use case#3 — Navigation
I love Pokemon. I like Apple and the stock market.
Why not build a screen where I can navigate between the two of them?
Same drill — play with it.
Did you notice the delay when you clicked the “APPLE STOCK” tab?
From the moment we click that tab, it takes some time for the tab to be highlighted. Again, this is a UX senstive action — that is, users want to get immediate feedback for clicking that tab while they are looking at it. Once they click that tab, we want to highlight it as fast as we can.
There’s a delay because React renders the long stock list once we click that tab, blocking the highlight from being painted.
Let’s tell React, “The initial render of that list is less important than highlighting the active tab!”
We change the useEffect
of the stock component from this:
to this:
The result:
Summary
For all of the examples I’ve just shown you, there are surely other ways to mitigate them, like virtualization, lazy loading, or even simple solutions like throttling. Still, the React team has supplied us with a built-in mechanism for concurrent renders, a mechanism that no other frontend library has built yet.
Built by the magicians at Facebook, the powerful React concurrent features enrich us with powerful tools that help us shape UX optimal applications and services optimize performance.
All the examples in this post can be found on Github, or on Codesandbox.
If you enjoyed this post, make sure to follow me on:
Medium: medium.com/@eilonmor1
Github: github.com/eilonmore
LinkedIn: linkedin.com/in/eilon-mor-990736150
Twitter: twitter.com/moreilon