We spoke with Matan Borenkraout about React Scheduler, Concurrent Mode, and what you can do to improve the performance of your React Applications. Matan demonstrated the effects of UI blocking work on an application and then refactored the application to use new, experimental APIs and substantially improve performance. Both TJ and I learned a fair bit about these new techniques. Matan is an excellent explainer of technology. We enjoyed him as a guest and will find a way to get him on another show.
In essence, user interface performance can be judged in part by the frame rate. Each frame represents a static snapshot of the user interface. When played at a full frame rate, the application experience is smooth and responsive. “Full frame-rate” in the browser is 60 frames per second.
Frame duration at full frame-rate is 16ms. If the underlying application consumes more than 16ms on the user interface thread, the user interface will be blocked. Blocking means dropped frames. One or two dropped frames from time to time isn’t going to be noticeable by most users under most conditions. The user interface will noticeably lag if blocking causes enough dropped frames. We discuss the effects of dropping frames on user experience at 8:14, followed by a demonstration.
The Setup
Matan prepared an application to demonstrate the experience impacts from a blocked UI thread (3:08). The application is a searchable list of Pokemon characters. Matan animated the title of the application with rotating colors as a visual representation of the UI layer. At the bottom, he placed a Frames Per Second (FPS) meter and a reference to the minimum recorded FPS. I’ve notated the screenshot below. You can view or use Matan’s example application on GitHub.
Demonstrating the Problem
As a user types in characters into the search box, the code searches Pokemon names and when a match is found, adds a background color. To trigger the issue, Matan programmed in 200ms of UI blocking work into the processing for each Pokemon character. You can imagine this as JavaScript processing required to load or set up a potential result item. The browser will avoid rendering frames if it is busy with other work, causing a degraded experience like temporarily freezing the user interface. In the image below, note the frames per second drops from 60 to 2.5 at the nadir. 2.5 frames per second is an abysmal user experience and may trigger the user to reload the whole page.
Matan sets the test up and launches the profiler at 9:38. The profiler contains helpful analytics to reveal what is happening between your code and the runtime engine. I’ve notated some of the crucial elements in the screenshot below. Software developers can better understand the issues contributing to poor user experiences by working with the profiler screens.
Blocking UI thread issues
At 12:43, we now have a functioning test case of blocked UI threads and have an understanding of what the symptoms are in the profiler. Matan adds to the test case by introducing some deferrable work for the browser to do. React developers should consider the nature of computational work and ask themselves if the work items are important enough to block the UI thread potentially, or if the work is deferrable to a more suitable time. By categorizing the discrete computational elements, the React developer can use tools and techniques to ensure they appropriately utilize the UI thread for the smoothest user experience.
At 18:07, Matan introduces the concept of debouncing, a software development technique that defers responding to user input until the user has paused their interaction for a certain period. In Matan’s example application, the user uses a search box to type in a partial Pokemon name. If the software developer implemented the feature naively, each onKeyPress event would trigger the search functionality. If the user typed in four characters, the search functionality triggers four times. If implemented with debounce, the functionality triggers once the user paused for say 100ms, removing three unnecessary search function triggers. Debouncing is a technique to use in your user interface programming. However, you must be careful to test the interactions and choose an appropriate amount of time for the pause period as too long gives the user a sluggish and unresponsive experience.
How React Scheduler helps you build smooth UIs
At 19:41, Matan introduces React Scheduler, a set of APIs designed to help you offload work and free up the UI thread. As of the publishing date of this article, the React Scheduler APIs are not widely used inside React but can be found in areas like useEffects. We liked that unstable APIs in React are prefixed with “unstable_” to indicate the early status. While the APIs will change before they are production-ready, the React Scheduler API concepts are essential to start learning now so you can plan on how to use them in your applications.
The Scheduler API works by allowing the software developer to designate an amount of time for the work to be deferred until it MUST be processed. The runtime uses this time value as an envelope to slip in the work task without blocking the user interface thread. If the runtime cannot process the task in the allotted period without blocking the user thread, the task will be assigned a priority of USER_BLOCKING and execute then. Note, this could still cause the user interface thread to block, but at least you gave it a chance to get worked into the processing flow. We discussed the considerations behind how long to defer work with the React Scheduler at 25:37 in the broadcast.
What happens under the covers
At 31:00, Matan discusses how React implements cooperative concurrency in a single-threaded environment using requestAnimationFrame and postMessage API. React runs in a while loop as long as there are fibers to render. Whenever the rendering process for a fiber is completed, it yields back to the browser by setting up a callback in the event loop. You can read more about this by searching for “Time Slicing React”. The scheduler APIs link up with this callback to know when there is a pause in render work.
At the moment, React can only schedule React work in its own userspace. This condition presents an opportunity for additional instructions to schedule processing by the runtime that could conflict with the React application developer’s instructions.
The React Scheduler API will soon connect with native APIs inside of Chrome to ensure all scheduling happens at a centralized point, removing potential conflicts in scheduling requests. At 37:59, Matan showed an example of using the Scheduler on the UI blocking code. You can see the improvement in the profiler as well as in the user interface responsiveness. We had a good discussion about the features and mechanisms following the demo.
When should you look to defer work?
At 48:43, TJ asked Matan to talk about when React developers should start to consider using the Scheduler APIs to improve performance. Matan offered beneficial advice to React developers on how to reframe their responses to performance problems. At the moment, memo and other techniques are the current best practices. When the Scheduler APIs become official, these skills will be an essential part of the React developer’s performance arsenal.
Key links and resources:
- Recording of React Application Concurrency, Main Thread Scheduling, And The React Scheduling API With Matan Borenkraout on YouTube
- Follow Matan on Twitter
- Main Thread Scheduling API Spec
- The new React postTask approach using the new postTask API
- Matan’s demo app github repo (already uses concurrent branch)
- Philipp Spiess’s Scheduletron3000 app demonstrating the new React features Concurrent React and the Scheduler
- Matan’s article describing the project