Non-blocking <canvas /> rendering with Concurrent React

Non-blocking <canvas /> Rendering in Concurrent ReactHow to split long tasks with generator functions and a scheduler for better rendering performanceReact Logo | Source: Wikimedia CommonsThe new React 18 provides concurrent features to make your…


This content originally appeared on Level Up Coding - Medium and was authored by Piotr Oleś

Non-blocking <canvas /> Rendering in Concurrent React

How to split long tasks with generator functions and a scheduler for better rendering performance

React Logo | Source: Wikimedia Commons

The new React 18 provides concurrent features to make your app more responsive. Unfortunately, it covers only the component rendering part — component effects still can block the main thread. In this article, I will show you how to use the use-transition-effect package to run heavy effects (like rendering on a canvas) concurrently.

The Basics

Let’s forget about React for a while to get a broader view of front-end performance issues. It will help with getting a deeper understanding of how concurrent features work.

What makes the front-end feel slow?

Front-end apps feel slow when the user is trying to interact with them, but the page is unresponsive because it’s working on something else.

When a single task takes too long to execute, it blocks the browser from responding to user input: the page feels slow.

What is a task?

A task is a unit of work that your browser does to render a frame.
It consists of the following steps:

These steps are often called a “rendering pipeline”

You can find more about the rendering pipeline in this web.dev article.

What is a long task?

A long task is a task that takes more than 50 ms. The 50 ms value is based on the user-centric performance model called RAIL. If a task takes more than 50 ms, user input feels delayed (that’s how our brain works 🧠).

If you think about the long task problem, the issue is not that the given task takes too long, but that it blocks other tasks.

The main problem here is not the performance but scheduling.
- Dan Abramov (Beyond React 16)

How to unblock the main thread?

There are two main strategies to unblock the main thread — parallelism and concurrency.

Different task running strategies

Parallelism is about running on multiple threads, so it may literally run multiple tasks at the same time on separate CPU cores.

Concurrency is about running on a single thread and quickly switching between tasks to create an illusion of running them in parallel.

Scheduling is a variant of concurrency that includes task prioritization.

Why concurrency/scheduling and not parallelism (WebWorker)?

The WebWorker API allows you to run JavaScript code on multiple threads. Unfortunately, WebWorker API has many limitations. The biggest one is the lack of DOM access. Also, you have to copy data to send it between threads, which can introduce a long task by itself.

From my personal experience, WebWorkers are good for data processing and crunching numbers, but hard to use for UI-related stuff. Making existing code work in the WebWorker environment is much harder than adjusting it for a scheduler.

TLDR; if WebWorker suits your case, that’s great, use it! Otherwise, use concurrency/scheduling.

What is required for concurrency?

Concurrency is about quickly switching between different tasks. To switch to another task, you have to interrupt/pause the current one. To interrupt the current task, you can use a generator function. The common technique is to pause every 10 ms, so the browser still has 6 ms to finish the rendering pipeline and meet the 16 ms deadline for 60 FPS (it’s 16 ms because 1s / 60 16 ms).

To decide which task to run, you need a scheduler.

Long Task vs Quick Tasks illustration
To unblock the main thread, you have to split long task into multiple short tasks

How to build a simple “scheduler”

I implemented a very simple version of a scheduler (it can run only one generator function at a time without prioritization). The idea is to run the generator function until we reach the deadline (10 ms), pause it, and schedule a function that will resume it, using setTimeout().

This implementation is far from being production-ready. For example, setTimeout() introduces 4 ms lag (minimum setTimeout delay), we can’t schedule multiple functions, and there is no prioritization (so it’s a simple concurrency). Fortunately, we can use the scheduler package developed by React team. It handles all of these scheduler features. Also, with a single scheduler, it’s less probable to starve resources (by competing schedulers).

The scheduler package

Starting from React 17, the react-dom package includes the scheduler package as a dependency. The scheduler package is used under the hood in React 18’s concurrent features like concurrent component rendering.

The concurrent component rendering algorithm is as follows:

  1. Start rendering a new Virtual DOM.
  2. If rendering takes longer than a threshold (for example, 10 ms), interrupt it. Otherwise, apply rendered Virtual DOM to the real DOM.
  3. The main thread is unblocked — run other high-priority tasks, like event handlers, and render a frame.
  4. If rendering has been interrupted, resume it, and go to step 2.

It splits the component rendering task into a few smaller, non-blocking tasks. Here is an example where you can see it in action:

It’s worth noting that currently, concurrent rendering requires memoization of expensive components (not sure why 🤔). You can achieve the same effect as the useDeferredValue() with the useTransition() hook, but it’s more verbose.

As I said at the beginning of the article, it covers only the component rendering part — component effects still can block the main thread. Let’s see how we can use the scheduler package to unblock the main thread in component effects.

Using the scheduler package

These are the most important bits of the scheduler API:

All functions are exported with the unstable_ prefix to emphasize that we should not expect that they will follow semantic versioning. I suggest re-exporting these functions if you want to use the scheduler API. This way, you have a single place to adapt in case this API changes in the future:

useTransitionEffect()

To integrate generator functions, scheduler, and React, I created the use-transition-effect package (npm, source code). You don’t have to upgrade to React 18 — it works with React 17 as well. I encourage you to look at the source code — it’s just ~100 lines of code. The API is similar to the useTransition hook from React:

startTransitionEffect lets you schedule a long-running effect without blocking the main thread. It expects a generator function as an argument, so you can yield to unblock the main thread:

Additionally, you can yield and return a cleanup function that will run on transition stop (including unmount):

stopTransitionEffect lets you stop the current long-running effect. You can use it as a useEffect cleanup:

isPending indicates when a transition effect is active to show a pending state:

The scheduler package exports the unstable_shouldYield() function that returns true if the current task takes too long. You can use it to decide when to yield:

If you want to update the state during a transition effect, you have to wrap this update with the unstable_runWithPriority() function from the scheduler package (with a priority higher than IdlePriority). Otherwise, the state update inside the transition effect will run when the transition effect ends:

Here is the previous example with primes computation implemented using useTransitionEffect():

Practical usage

I have to admit that finding primes on the front-end is not a practical example. I choose it because of its simplicity. But there are more practical ones:

Rendering on a <canvas>

If you render a lot of data-points/graphs, sooner or later, you will hit performance issues. You could try to use a WebWorker and OffscreenCanvas, but as writing this article, OffscreenCanvas is not well supported. Also, you would have to make non-trivial changes to adapt existing code.

With the useTransitionEffect() , you can render incrementally or on the background canvas without blocking the main thread.

Processing data

Sending data from/to a worker requires data serialization. If you have to process relatively big data, just sending it from/to a worker can cause a long task.

With the useTransitionEffect(), you don’t have to serialize any data. You can process it incrementally without blocking the main thread.

Conclusion

Scheduling is a really powerful technique to deliver responsive user interfaces. We have to remember that some of our users have much slower devices — so even if your website feels responsive, it may be because you have a high-end computer for development. It’s a good idea to enable CPU throttling (DevTools / Performance tab in Chrome) and see how it behaves.

I think it’s ok to use the unstable_ API. I don’t think it will change dramatically in the next React releases. I suspect that React team waits for Scheduler API to be available in all browsers. Then they will decide if they want to keep their implementation as a thin abstraction over browser API, or if they want to get rid of it altogether. Either way, migration should be pretty easy.

If you like the use-transition-effect package, please give it a star ⭐️ And if you enjoy the content, I welcome you to follow my Medium account — this is my first article, and I plan to write more 👨‍💻


Non-blocking <canvas /> rendering with Concurrent React was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding - Medium and was authored by Piotr Oleś


Print Share Comment Cite Upload Translate Updates
APA

Piotr Oleś | Sciencx (2022-06-26T13:04:31+00:00) Non-blocking <canvas /> rendering with Concurrent React. Retrieved from https://www.scien.cx/2022/06/26/non-blocking-canvas-rendering-with-concurrent-react/

MLA
" » Non-blocking <canvas /> rendering with Concurrent React." Piotr Oleś | Sciencx - Sunday June 26, 2022, https://www.scien.cx/2022/06/26/non-blocking-canvas-rendering-with-concurrent-react/
HARVARD
Piotr Oleś | Sciencx Sunday June 26, 2022 » Non-blocking <canvas /> rendering with Concurrent React., viewed ,<https://www.scien.cx/2022/06/26/non-blocking-canvas-rendering-with-concurrent-react/>
VANCOUVER
Piotr Oleś | Sciencx - » Non-blocking <canvas /> rendering with Concurrent React. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2022/06/26/non-blocking-canvas-rendering-with-concurrent-react/
CHICAGO
" » Non-blocking <canvas /> rendering with Concurrent React." Piotr Oleś | Sciencx - Accessed . https://www.scien.cx/2022/06/26/non-blocking-canvas-rendering-with-concurrent-react/
IEEE
" » Non-blocking <canvas /> rendering with Concurrent React." Piotr Oleś | Sciencx [Online]. Available: https://www.scien.cx/2022/06/26/non-blocking-canvas-rendering-with-concurrent-react/. [Accessed: ]
rf:citation
» Non-blocking <canvas /> rendering with Concurrent React | Piotr Oleś | Sciencx | https://www.scien.cx/2022/06/26/non-blocking-canvas-rendering-with-concurrent-react/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.