Advanced JavaScript and React Performance Engineering
Concurrent Rendering Features
Interruptible Rendering
You've seen how React's Fiber architecture breaks rendering into small, manageable chunks. This isn't just an internal optimization; it unlocks a powerful new capability:
Imagine a user typing into a search bar that filters a very long list. In the old model, every keystroke would trigger a re-render of that entire list. If the list is large, the filtering logic could take a few hundred milliseconds. Since JavaScript is single-threaded, this heavy work blocks the main thread, making the input field itself feel laggy and unresponsive. The user types, but the letters appear with a noticeable delay.
Concurrent rendering solves this by allowing React to prioritize. It understands that updating the text in the input field is an urgent update, while re-rendering the filtered list is a non-urgent update that can wait. React can start rendering the list, but if the user types another letter before it finishes, it will throw away the unfinished work and start again with the new input. This ensures the UI remains fluid and responsive to the user's actions.
Marking Updates with useTransition
To tell React which updates can be interrupted, we use the useTransition hook. It's the primary tool for marking state changes as non-urgent.
The hook returns an array with two items: a boolean isPending flag and a function, typically called startTransition.
const [isPending, startTransition] = useTransition();
Any state updates you wrap inside the startTransition function are marked as transitions. React knows it can delay these or interrupt them if a more critical update comes in.
Let's apply this to our search filter example. We have two pieces of state: the text in the input box and the filtered list that gets displayed.
import { useState, useTransition } from 'react';
function App() {
const [isPending, startTransition] = useTransition();
const [inputValue, setInputValue] = useState('');
const [filterTerm, setFilterTerm] = useState('');
const handleInputChange = (e) => {
// Urgent: Update the input field immediately
setInputValue(e.target.value);
// Non-urgent: Kick off a transition to update the list
startTransition(() => {
setFilterTerm(e.target.value);
});
};
// Assume FilteredList is a component that renders a large list
// based on the filterTerm prop.
return (
<div>
<input
type="text"
value={inputValue}
onChange={handleInputChange}
/>
{isPending && <p>Filtering list...</p>}
<FilteredList term={filterTerm} />
</div>
);
}
In this code, updating inputValue is an urgent update. The input field feels instant because its state change is not wrapped in startTransition. However, the expensive update to filterTerm is wrapped. React can start re-rendering FilteredList, but if the user types again, it will prioritize updating inputValue and restart the transition. The isPending flag is useful for showing a loading indicator, letting the user know that the background operation is in progress.
A key takeaway:
useTransitionlets you split a single user action into two updates: one urgent and one non-urgent.
Deferring Values with useDeferredValue
useTransition is great when you control the state-setting function. But what if the value causing the slow re-render comes from a parent component as a prop? You can't wrap a prop change in startTransition.
This is where useDeferredValue comes in. It lets you create a "deferred" version of a value that will "lag behind" the original. React will render first with the old value, keeping the UI responsive, and then trigger a second, non-urgent render with the new value once the urgent updates are finished.
import { useState, useDeferredValue } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
return (
<>
<SearchInput value={query} onChange={e => setQuery(e.target.value)} />
<SearchResults query={query} />
</>
);
}
function SearchResults({ query }) {
// Create a deferred version of the query prop.
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
// The list will now re-render based on deferredQuery,
// which is a non-urgent update.
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<FilteredList term={deferredQuery} />
</div>
);
}
Here, SearchResults receives the query prop. The useDeferredValue(query) call tells React that updates to this value are low priority. When the user types, query updates immediately, but deferredQuery keeps its previous value until React has time to process the change. We can even compare query and deferredQuery to know if the data being shown is stale, allowing us to gray out the old results for better UX.
Transitions vs Throttling
Before concurrent features, developers often used to handle performance issues from frequent events. While useful, these techniques are fundamentally different from transitions.
- Throttling limits how often a function can run. For example, it might execute a function at most once every 200 milliseconds. This is a fixed rate limit.
- Debouncing waits for a pause in events before running a function. If a user is typing, it waits until they stop for, say, 300 milliseconds before executing.
Transitions are a more robust solution. Unlike debouncing, they don't introduce an artificial delay. The feedback can start rendering immediately. Unlike throttling, they are not tied to a fixed time interval. A transition can be interrupted at any moment by a higher-priority update, making the UI feel more adaptive and intelligent. On a fast device, the deferred update might feel instantaneous. On a slow device, React will gracefully prioritize user input to prevent jank, adapting to the user's hardware.
It's time for a quick review to see what you've learned.
What is the primary problem that "interruptible rendering" in React is designed to solve?
You are building a component with a search input. The state update for the input's text value should be wrapped in startTransition to keep the UI responsive.
By marking updates as non-urgent, you give React the power to prioritize, keeping your application fluid and responsive even during heavy lifting.