GSD

Concurrent React - The Future of Web Apps

Posted by Zain Sajjad on September 15, 2023

Post updated by Maab Saleem.

What is concurrency?

Concurrency is an optimization technique which enables a software program to perform multiple tasks simultaneously. Concurrency allows for more efficient usage of system resources, which may improve the overall performance and scalability of an application.

For example, a concurrent web server can receive and process several requests at once. This allows it to serve more clients in less time. Similarly, a video game uses concurrency to update its audio, video, and graphics at the same time. This ensures a lag-free, immersive experience for the player.

Concurrency is also essential for enhancing the performance and responsiveness of user interfaces. In modern web apps, UI rendering can be a resource-intensive process. To deliver a seamless and engaging user experience, UI apps must perform different rendering tasks concurrently.

For instance, a web app may asynchronously load data and images in the background, as the UI is rendering and reacting to user input. This can lead to smoother transitions, faster load times, and happier visitors.

A history of concurrent rendering with React

Before 2018, React apps couldn’t perform concurrent rendering. The reason was that React’s rendering model was based on a global event loop that updated the entire UI tree at once. This meant that even the smallest of UI updates would block the event loop, causing the app to slow down. For example, a component requiring a lot of processing would slow down the rendering process, and make the app unresponsive.

Additionally, the old rendering model didn’t allow developers to prioritize updates. High-priority updates could get delayed due to lower-priority updates, reducing the overall interactivity of the app. As workarounds, developers would split up components or use other optimization techniques to ensure that the UI remained responsive.

Enter concurrent mode. In 2018, the React team announced an experimental mode to address concurrency issues. It featured a priority-based scheduler which allowed React to execute high-priority tasks before those with lower priorities. It also introduced incremental rendering, which splits the rendering work into smaller chunks, and interleaves them with other tasks to enhance the responsiveness of an app.

See how Butter's simple content API works with your React app.

What is concurrent rendering in React 18?

With the release of React 18, concurrency has become an intrinsic part of the React core. The experimental concurrent “mode” has been replaced by a concurrent renderer. However, concurrent React isn’t enabled by default. It requires explicit opt-in, meaning that it’s only enabled when you use a concurrent feature.

Concurrency in React 18 isn’t a new feature, rather an internal technique that allows React to render multiple versions of the UI simultaneously. Under the hood, React uses priority queues and multiple buffering to implement concurrency, but these concepts are abstracted away from the public APIs. 

What does concurrency solve?

Concurrent React makes rendering interruptible. This allows React to pause a rendering task, or abandon it completely, without disrupting the consistency of the UI. Interruptible rendering enables React to render new screens in the background, without blocking the app. The result is a fluid, immersive UI that can react to user input, even when it’s performing a time-consuming rendering operation.

Another problem that concurrent React solves is related to hiding and showing a component. Traditionally, to hide and show a component, you’d have to either add or remove it from the tree, or toggle its visibility using CSS. Both approaches come with trade-offs, like lost UI state and performance overhead.

Concurrent React offers a third option via the Offscreen component, which hides the component, and deprioritizes its rendering. The hidden component is only rendered when the app is idle, or when it becomes visible again. Offscreen can be used to implement useful features, like reusable state and instant transitions.

React 18 concurrency features

Let’s look at a few more concurrency features that React 18 introduces.

Transitions

React 18 presents a new concept known as transitions, which can be used to differentiate between urgent and transition (low-priority) updates. An urgent update requires an immediate, intuitive response. User interactions, like clicking, typing, and entering are examples of urgent updates.

A transition update has a lower priority than an urgent update, which means that it can be delayed or interrupted if necessary. Transition updates are typically used for non-critical UI changes, such as animations or loading indicators.

React 18 offers two APIs to categorize urgent and non-urgent updates:

useTransition

The useTransition hook can be used to mark certain updates as transitions (i.e. having low-priority). Here’s how its signature looks:

const [isPending, startTransition] = useTransition()

The returned array contains two elements. The isPending Boolean flag indicates whether the state update is pending. The startTransition function can be used to mark a certain update as a transition. Consider the following code snippet:

import { useState, useTransition } from 'react';
function SampleContainer() {
  const [isPending, startTransition] = useTransition();
  const [currentTab, setNextTab] = useState('contact');
 
  function chooseTab(next) {
	startTransition(() => {
  	setNextTab(next);
	});
  }
  // ...
}

In the above example, we are calling useTransition at the top which returns the startTransition function. Later on in the code, we are using the returned function to mark the setNextTab function as a transition.

startTransition

The startTransition API can be used to mark updates of an input event as urgent and non-urgent (transitions). Here’s how its signature looks:

startTransition(scope)

The scope argument is a function used to update state. For example, consider this code snippet:

import {startTransition} from 'react';
setInput(text); //urgent operation, show the user what they typed
startTransition(() => {  //wrap this function around any non-urgent state updates
  setQueryText(text); //this will be a transition
});

In this example, we are marking the setInput operation as urgent, so that the user is immediately shown what they have typed. However, we are marking setQueryText as a low-priority state change which can be interrupted (e.g. to handle new user input).

Automatic batching

Batching is a React feature which performs multiple state updates in a single rendering operation. In earlier versions of React core, by default, only the state updates defined inside React event handlers were batched.

React 18 comes with automatic batching, which batches all updates by default, regardless of their origin. This includes updates inside promises, timeouts, and native event handlers. Automatic batching significantly reduces rendering iterations, which enhances performance and responsiveness.  

useDefferedValue

useDefferedValue is a hook used to defer the re-rendering of a non-urgent part of the user interface. The syntax looks like this:

const deferredValue = useDeferredValue(value)

The value parameter indicates the part that you want to defer. Let’s look at an example:

function CustomerList({ customers }) {
  const deferredCustomers = useDeferredValue(customers);
  return (
	<ul>
  	{deferredCustomers.map((customer) => (
  	  <li>{customer}</li>
  	))}
	</ul>
  );
}

In the above example, we are passing customers to the useDefferedValue function. This instructs React to associate a low priority to updates related to customers. The deferred render of state updates is also interruptible.

Suspense

Suspense is a powerful tool in React that enables you to display a fallback UI while a component is loading. Once the component has loaded completely, the fallback UI is replaced by the actual UI. For example, you may define a Loading component as the fallback UI for a component that loads data asynchronously.

While the asynchronous component is being loaded, React will render the fallback component, which displays a loading screen to the user. Consider this code snippet:

import { Suspense } from 'react';
 
function MyComponent() {
  return (
	<div>
  	<Suspense fallback= {<Loading />}>
    	<AsyncComponent />
  	</Suspense>
	</div>
  );
}
 
function AsyncComponent() {
  const data = fetchData(); // asynchronous data loading
 
  return (
	<div>{data}</div>
  );
}

In this example, MyComponent wraps the AsyncComponent component with Suspense. Suspense will render the fallback Loading component until the AsyncComponent has finished loading data. Once the data is available, the AsyncComponent will render normally with the loaded data.

Suspense in React 18 comes with two major features: streaming server rendering (or streaming HTML) and selective hydration. With streaming server rendering, the initial (server-rendered) HTML sent to the client can contain placeholders (as defined by the fallback component) for Suspense components.

These placeholders are replaced with the actual content, as it becomes available. By wrapping time-consuming components inside Suspense, and letting all smaller components load as usual, you can reduce the time the user needs to wait before seeing content on the page.

Selective hydration allows React to hydrate all the smaller components, before the content for the Suspense component has been loaded. The hydration for the Suspense components happens after the placeholder HTML is replaced by the actual content.

How to opt into concurrency

Concurrent React isn’t enabled by default in React 18. To opt in, upgrade all your NPM packages to React 18, and plug any concurrent feature into your app. For example, you may start using the startTransition API to mark certain updates as interruptible. Or you can use the useDeferredValue to defer certain re-rendering operations.

Another way to opt in is by using the new root API (ReactDOM.createRoot). It optimizes several areas of the application by enabling concurrency.

import * as ReactDOM from 'react-dom';
import App from 'App';
 
const container = document.getElementById('app');
 
// Create a React root
const root = ReactDOM.createRoot(container);
 
// Use the root to render
root.render(<App />);

Yet another way to opt in is by using a library or a framework that encapsulates concurrency features. For example, Next.js 13, which is built using React 18, unlocks several concurrency-related features, like startTransition and streaming server-side rendering.

What are the benefits of enabling React concurrent rendering?

Concurrent rendering with React promises several benefits for frontend developers.

Enhanced user experience

Concurrent React is packed with various features to customize rendering operations. You can prioritize urgent tasks over non-urgent updates, and perform selective hydration using Suspense. These optimizations result in a smoother and more intuitive UI.

Better performance

Concurrency in React helps in reducing the time it takes to render updates, especially for large applications. By splitting rendering work into smaller chunks, and using priority-based scheduling, React 18 achieves more in less time, boosting overall performance.

Efficient resource usage

Concurrent React facilitates a more efficient utilization of resources. With concurrency enabled, React can spend valuable CPU and memory cycles on prioritized rendering tasks, and suspend execution of non-urgent tasks whenever needed.

Improved responsiveness

Developers can use concurrency to build applications that can act on user input even if a (non-urgent) rendering operation is being executed. They can also display server-rendered HTML to the user, while some components are still loading. These features enhance the overall interactivity of an application.

See how Butter's simple content API works with your React app.

Easier to develop complex applications

Prior to the release of React 18, developers had to resort to implementing workarounds to optimize complex applications that contained a large number of components. With React 18, they can leverage built-in concurrency features to effectively reduce technical debt and improve code maintainability.

Support for new features and use cases

Concurrency in React opens new possibilities for features and use cases that were previously difficult to implement. For example, it can make it easier to implement performant, real-time collaborative editing, where multiple users can edit the same document simultaneously.

Getting started with React concurrency

Concurrency represents a paradigm shift in React development. To get started, it’s important to understand the new concepts and features, most of which were covered in this post. Here are a few things to consider while adopting concurrent React.

It’s a breaking change

Concurrent rendering is interruptible, making it different from the traditional synchronous rendering approach used in React. You may notice that some components are behaving differently with concurrency enabled. To ensure a smooth transition for your components, carefully evaluate the change impact and formulate a migration plan.

Experiment with the new APIs

Try out the new features and APIs of React 18 by implementing them in small test cases or experimental components. This will help you gain a better understanding of how concurrency works and how to use it effectively. For example, you may write a test application to see startTransition in action.

Leverage concurrency to optimize

Once you have a vivid idea of how concurrent features work, it’s time to use them to optimize your components. Use the new root API to create your root object, introduce the suspense component to customize loading, and leverage the useTransition hook to prioritize updates. If you are using a framework, like Next.js or Gatsby, you’ll likely find built-in support for all React 18 features.

Test, test, test

Incrementally introduce concurrency features to your application to limit the chances of bugs and exceptions. Be sure to thoroughly test and evaluate your app’s performance after every iteration. You may also use profiling tools to identify any bottlenecks or improvement avenues.

Final word

Concurrent React allows frontend developers to develop fast and reliable interfaces, using features like customized server-side rendering, selective hydration, priority-based scheduling, and suspense-driven loading. The aim of this post was to offer a comprehensive guide to concurrent React; we hope you found it helpful.  

Sign up to receive more articles about ButterCMS and React.
    
Zain Sajjad

Zain is a Senior Frontend Engineer at Peekaboo Guru. He is passionate about anything and everything React, React Native, JavaScript & Mobile Machine Learning.

ButterCMS is the #1 rated Headless CMS

G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award

Don’t miss a single post

Get our latest articles, stay updated!