A team wrapped almost every setState in their React 19 app in startTransition to "keep the UI responsive". Under fast typing, results now lag far longer than before and the app burns more CPU. What is the mechanism, and how would you fix it?
answer
- priority only exists relative to something
- marking everything marks nothing
- interrupted low-priority work restarts
- restarts repeat the render cost
basics
~20 sPriority is relative, so marking everything low-priority prioritises nothing. Each urgent keystroke preempts the in-flight low-priority render, which restarts from scratch, repeating work and delaying the commit until typing pauses. Mark only the expensive downstream update.
solid answer
~50 sLow priority is a promise that an update may be delayed and restarted, and the team has now made that promise about updates the user is directly waiting on. Every keystroke schedules urgent work that preempts the in-flight transition render; React discards the partial tree and restarts against the newest state, so under sustained typing the low-priority render repeats work without ever reaching a commit until the user pauses. That is both the lag and the extra CPU. It is also self-defeating in a second way: if every update is a transition, there is no relative ordering left for React to exploit. The fix is to be selective — keep anything that is direct feedback urgent (the input's own value, a toggle, opening a modal), wrap only the expensive downstream recomputation, and then attack the render cost itself, since prioritisation reorders work but never reduces it.
go deeper
Remember that only the expensive downstream update belongs in a transition; the value the user just typed or toggled must stay urgent so it appears immediately.
Explain why the interrupted low-priority render restarts rather than resumes, and why that makes repeated preemption cost real CPU while delaying the commit.
Diagnose it from evidence — continuously true pending state, repeated renders with no commit, higher CPU with no earlier paint — and prescribe narrowing the marking before touching anything else.
Set the team rule that priority reorders work and never reduces it, so scheduling is never the answer to a cost problem, and make measuring the bottleneck a precondition for reaching for transitions at all.
## Priority is a ranking, not a setting React's scheduler does not have an absolute notion of fast or slow. It has a set of pending lanes on a root and it picks the highest one. "Everything is a transition" collapses that ranking: there is nothing left for React to schedule *ahead* of anything else, so you have paid the costs of the concurrent path while keeping none of the ordering benefit. When a team says responsiveness got worse after adopting transitions everywhere, this is nearly always the root cause. ## The restart tax under sustained input The second cause is concrete and measurable. A transition render is interruptible, and interruption by a higher-priority update means *discard and restart*, not pause and resume — the partial tree was built from state the urgent update just invalidated. Now consider a user typing at six characters per second while the transition render takes 120 ms: 1. keystroke arrives, urgent lane rendered and committed, transition scheduled; 2. transition renders for roughly 160 ms of wall clock in short slices; 3. next keystroke lands after 160 ms, preempting it; 4. React throws away the partial work and starts over against the newest query. Under that pattern the transition may never commit until typing stops. The user sees stale results for the entire typing burst, and every abandoned slice was real CPU that produced nothing. Total CPU is strictly higher than a synchronous render of the same work — the tradeoff is deliberate, and it only pays when the thing you protected (input latency) is what the user actually notices. ## What should stay urgent The rule that holds up in review: **anything the user is directly manipulating stays urgent; only the expensive derived work becomes a transition.** ```jsx function handleChange(event) { const next = event.target.value; setQuery(next); // direct feedback — must render this frame startTransition(() => { setResults(filter(items, next)); // derived, expensive, may lag }); } ``` Inverting that is the classic bug: wrapping `setQuery` in the transition makes the input itself laggy, because now the character the user typed is the thing React is willing to delay and restart. Toggling a checkbox, opening a menu, and pressing a button are in the same category — they are feedback, not derived work. ## Recognising the symptom in the wild Signals that a codebase has over-transitioned: - `isPending` is true almost continuously during interaction rather than in short bursts; - controlled inputs feel spongy — characters appear a beat after they are typed; - profiling shows the same component tree rendering many times per interaction with no commit in between; - CPU during typing is higher than before transitions were introduced, while nothing on screen updates sooner. ## Fixing it properly Three steps, in order. First, **narrow the marking**. Remove `startTransition` from everything that is direct feedback or cheap. What remains should be a small set of genuinely expensive derived updates. Second, **reduce the work per attempt**. Because restarts repeat the render, the cost of a restart is the cost of the render — so cheapening the render pays twice under interruption. Render fewer components per keystroke, and keep the expensive derivation out of the render path where you can. Third, **check whether prioritisation is even the right tool**. If the expensive part is a network round trip rather than render cost, priority does nothing for it. If the expensive part is one component's body, slicing cannot break it up. If the result genuinely should not be recomputed on every keystroke at all, the honest answer is fewer updates, not lower-priority ones. ## The sentence to say out loud "Transitions reorder work; they never remove it. Marking everything low priority removes the ordering signal and adds restart cost, so we mark only what the user can tolerate lagging, and we still have to make the render cheap." That single framing answers both halves of the question and tells the interviewer you have operated this in production rather than read about it.
- How would you confirm this diagnosis rather than assume it?Record an interaction while typing and look at whether the same subtree renders repeatedly with no commit between renders, and whether `isPending` stays continuously true across the burst. Compare CPU during typing before and after the transitions were added. Repeated renders without commits, plus higher CPU and no earlier paint, is the signature of restart churn rather than genuinely deferred work.
- Is there a case where wrapping a cheap update in a transition is still correct?Yes — when the update is not cheap in isolation but triggers something that suspends, such as navigating to a route whose data is not ready. Marking it a transition lets React keep the current screen visible instead of falling back, even though the state change itself is trivial. The judgement is about what the update causes downstream, not the setter's own cost.
- The expensive part turns out to be a network request, not rendering. Does any of this help?No. Priority orders React's own render work; it cannot make a request return sooner. The lag there is data latency, and the levers are request timing, caching and what you show while waiting. Reaching for transitions in that situation is a sign the bottleneck was never measured.
saying these in an interview costs you the question
- Says more transitions always means a more responsive app
- Wraps the controlled input's own value in a transition
- Thinks a transition reduces the total work React does
- Believes the interrupted render resumes rather than restarting
- Assumes priority can speed up a slow network request