React concurrent rendering explained
What Concurrent Rendering Means
In this post, I try to explain what concurrent rendering means in React.
First, let’s clear up a common misconception.
Concurrent rendering does not mean that React renders multiple components at the same time.
JavaScript is single-threaded, and React cannot change that. Once synchronous JavaScript starts executing, React cannot interrupt it.
The Three Phases of a React Update
React has 3 phases/steps performed on a state update
1. Render phase
This is when React calls the functional component or render method. This means JavaScript is executed and React has to wait till it completes, i.e. till the function returns a virtual representation of the new DOM.
2. Reconciliation
This is when React compares the current virtual representation of the DOM with the previous version, to identify the differences. It determines what new nodes needs to be added, what nodes need to be updated and what needs to be deleted a.k.a Mutations
3. Commit
This is when React applies the mutations to the original DOM.
When Does the UI Update?
Now, after step 3, do you think the UI is immediately updated? If you said No, you are correct.
The DOM updates are then seen by a user only after browser repaints the screen. React cannot trigger a browser repaint by itself directly. It cannot tell the browser that its ready and go ahead, repaint. The browser repaints itself depending on the refresh rate of the screen.
For example, if your screen has a refresh rate of 60 FPS, your browser repaints every 16 milliseconds.
So, all three steps we discussed above should be done within a 16ms window for it to be visible on the next screen paint. What if JS takes more than 16ms to run ? Paint is skipped!
Concurrency Explained
Now when React says that its concurrent, what it means is that React is capable of stopping/resuming/cancelling any of these 3 steps in favor of more urgent UI updates.
Read that again. That is the core of concurrency in React.
Lets take an example.
React provides a new hook named useDeferredValue. By itself this hook is not of much use. You need to pair it with React.Memo or React.Suspense to see its true power. We will use React.Memo since it will be easier to explain concurrency.
Example Using useDeferredValue
import { useState, useDeferredValue, memo } from "react";
const SlowList = memo(function SlowList({ query }) {
const items = [];
for (let i = 0; i < 20000; i++) {
if (i.toString().includes(query)) {
items.push(
<li key={i}>{i}</li>
);
}
}
return <ul>{items}</ul>;
});
export default function App() {
const [text, setText] = useState("");
const deferredText = useDeferredValue(text);
return (
<>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Type a number"
/>
<p>Input value: {text}</p>
<p>Deferred value: {deferredText}</p>
<SlowList query={deferredText} />
</>
);
}
What Happens When You Type Quickly
Consider what happens when a user starts typing rapidly into the input field.
- User types 1
This is an urgent update. React processes it right away so the input remains responsive. -
Each update schedules work for
deferredText
Updates that depend ondeferredText—like renderingSlowList—are marked as non-urgent. React plans to render them when there is time. -
React begins render cycle of
SlowListusing an updated deferred value
React starts render cycle forSlowListfor"1" -
Before the render cycle completes, a new state update arrives
Now the user types"12". React pauses the render cycle of Slow List. This pause could be anywhere i.e. after render step, or after reconciliation step. -
React commits urgent state update
Thetextstate updates immediately again and input gets updated. -
React resumes rendering SlowList
Now React returns to render cycle of SlowList. If Deferred value has not changed yet, it continues from where it left off, but if it did change, React throws away the current render cycle and starts a new render cycle with the new deferred value. So you will only see Slowlist for value "12"
React renders components top-down. If component A renders component B, and B renders component C, React can interrupt rendering at any point in this tree— as long as the interruption is caused by an urgent update.
Non-Concurrent Rendering
How This Differs from Non-Concurrent Rendering
Before concurrent rendering, React used a fully synchronous rendering model. Once React started rendering an update, it had to finish that work and commit it before handling any new updates.
Using the same example as above, here is how the behavior would differ without concurrency.
-
The user types
"1"
React begins renderingSlowListfor"1". This render must run to completion. -
The user types
"12"while rendering is in progress
React cannot pause or interrupt the ongoing render. So input will not update immediately -
The UI remains blocked
The browser cannot repaint until the render completes. -
React commits stale work
React commits the result for"1"even though it is already outdated. -
React processes the next update
Only after committing does React begin rendering"12".
Final Note
That's it, that is all there is to React and its concurrency. This example was over-simplified for the sake of explaining the core concept. But always remember that React cannot stop JavaScript execution once it has started. So if your non urgent state update triggers a heavy JS execution, your UI will still hang. React cannot make slow JavaScript run faster.
Comments
Post a Comment