Concurrent rendering is a capability which allows a UI library to prepare a new version of the UI in the background while keeping the current version interactive. React introduced concurrent rendering a few years ago but until now some of the features are still at the experimental stage. Since then, SolidJS and some other libraries have borrowed the ideas and implemented similar concurrent features.
In this article, we will study concurrent rendering in SolidJS, which is a library for building web applications that are small and extremely fast. If you are new to Solid but already familiar with React you may want to read an introduction to SolidJS first.
Why concurrent rendering?
Before learning how to use concurrent rendering, you need to understand why such a capability is beneficial.
By default, rendering happens synchronously. When the user performs a certain action, say clicking on a button, an event handler will run which usually involves some computation and changing something in the application state which, in turn, causes the UI to update. This is great when everything within the event handler happens quickly, since the user can instantly see the result of their action.
But sometimes an event is inherently slow. For example, we may need to load a module because of code splitting. We may have to fetch some data from the backend. Or we may have a lot of expensive computations to execute. What would happen in these situations? With synchronous rendering, there would be a period of time during which the "old" UI is no longer available but the "new" UI is not yet ready and thus not interactive. By contrast, concurrent rendering can greatly improve the user experience because it allows the user to continue using the current version of the UI as usual while a new version is being prepared behind the scenes.
Concurrent rendering in SolidJS
Generally speaking, you can take advantage of concurrent rendering in two kinds of situations:
You need to wait for something such as a dynamically imported module or some data being fetched from the backend. In this case, you can create a resource to handle the loading of the dependency and use a
Suspense
component to specify the boundary of the part of the UI to be rendered asynchronously.You have a lot of computations to run, for example your application may contain thousands of fine-grained components and everyone of them needs to recalculate a computed value. In this case, you can opt for Solid's time slicing feature which breaks down the computation workload into small chunks and executes them in the background.
In the subsequent sections, we will study these use cases one by one through some examples.
Code splitting
First, let's see an example of code splitting without using Suspense.
In this example, we have a Counter component that is lazily loaded when the user clicks on the Start button for the first time. To do that, we use Solid's lazy()
function to wrap the dynamic import statement. Here we create a promise to simulate a two-second delay when loading the module. The first time you click on the Start button, you would notice that nothing seems to happen for a few seconds while the module is being loaded.
We can make the user experience a bit better by wrapping our lazily loaded Counter inside a Suspense
component and specifying a fallback UI to render as the module is being imported:
<Suspense fallback={<p>Loading...</p>}>
<Counter />
</Suspense>
What happens here is that the lazy()
function internally creates a resource to manage the dynamic import. The resource informs the Suspense component to render the fallback UI, and later notifies it to render the expected UI when loading finishes.
Data fetching
This is by far the most important use case of concurrent rendering. In the following example, we have a view that shows a list of items. Clicking on an item brings the user to another view which fetches the item's details and displays it. The traditional approach, as shown here, offers a poor user experience when the network connection is slow because the user only sees a loading indicator and is unable to use the app when loading is in progress.
Let's now use concurrent rendering to allow the user to "stay in the past" by making a couple of changes as follows:
Firstly, we need a Suspense
component that encompasses both the item view and the list view so that it can retain the list view in the UI when the item view is being prepared.
<Suspense>
<Show
when={selectedItem()}
fallback={<ListView items={ITEMS} onSelect={setSelectedItem} />}
>
<ItemView item={selectedItem} />
</Show>
</Suspense>
Secondly, we need to inform Solid that rendering the item view is not the highest priority so it should not render it immediately but should have a transition period until the resource resolves. To do so, we can call Solid's useTransition()
which returns a signal indicating whether the resource is still pending and a function to kickstart the transition.
function ListView(props) {
const [loading, setLoading] = createSignal<string>();
const [pending, start] = useTransition();
const handleClick = (item: string) => {
setLoading(item);
start(() => props.onSelect(item));
};
return (
<ul>
<For each={props.items}>
{(item: string) => (
<li onClick={() => handleClick(item)}>
{item} {pending() && loading() === item ? "(loading...)" : ""}
</li>
)}
</For>
</ul>
);
}
In the click event handler above, it is important to note that we don't want Solid to immediately render the item view but we do want an immediate indicator of which item being loaded. That's why only the second statement is wrapped in the transition.
Time slicing
Time slicing in Solid could be helpful when your application has a large number of reactive primitives to compute before rerendering the UI. However, I couldn't think of any application like that in the real world. I suppose most users wouldn't need this feature, and that's why Solid doesn't enable scheduling by default. To use time slicing, you need to explicitly call enableScheduling()
, otherwise the scheduler will be tree shaken from the application bundle.
However, let's still look into a contrived example to understand how to use time slicing. In this example, we have a range input that controls the number of items to render. Each item has a memo with a simulated long calculation time. When you use the input to increase the number of items, you should notice that the input becomes unresponsive for a while until all items have been rendered.
Time slicing breaks down the computation workload into small chunks and executes them only when the browser isn't busy with higher priority updates such as user inputs. As mentioned, we need to call enableScheduling()
first. After that, use startTransition()
to wrap low priority updates. In this case, we need to inform Solid that the creation of the items is of lower priority and thus should be interruptible. Now you should see that, as the user moves the slider, its value changes immediately and the input stays responsive even though it still takes a long while to render the list.
Conclusion
In this article, we have learned about concurrent rendering and why you may want to use Solid's concurrent features. In summary, there are three main scenarios for concurrent rendering. For code splitting, use a Suspense component to render a fallback UI while loading a module. For data fetching, use Suspense and transition to retain the current UI while the new UI is being prepared. Finally, you can consider enabling time slicing if you ever need a large number of reactive primitives to compute in your app.
Top comments (0)