skip to content

You wrapped an expensive list update in React 19's startTransition, but typing in the search field is still visibly janky. What can make a transition fail to restore responsiveness?

level: seniorimportance: should knowfreq 45%

answer

  1. priority, not cost
  2. React yields between components, not inside one
  3. did the update actually get marked?
  4. commit is synchronous and uninterruptible
  5. the urgent path may render it too

basics

~20 s

Transitions change priority, not cost. React can only yield between units of work, so one component with a very slow render still blocks a frame; and updates can escape the transition — scheduled outside its callback, or forced synchronous — leaving the whole render urgent again.

solid answer

~50 s

Three families of cause. First, the work is indivisible: React yields between components, so a single component doing a heavy synchronous computation or rendering ten thousand rows in one pass blocks the frame no matter what priority it has. Second, the update never actually became a transition — a setter called outside the `startTransition` callback, or in a `setTimeout` or event callback that runs later, is urgent again. In React 19 an async function passed to `startTransition` keeps post-`await` updates in the transition, which was not true in React 18, so version matters here. Third, the cost is not in the transition at all: expensive synchronous work in the event handler before the setter, an urgent render path that walks the same heavy subtree, or a costly commit — commit is synchronous and never interruptible. The fix is usually to shrink the work, not to re-mark it.

go deeper

for a junior

Know the one-sentence limit: startTransition changes when work runs relative to other work, never how much work there is. If the render is simply too big, priority marking will not save it.

for a middle

Explain the yield granularity — React pauses between components, not inside one — and list the ways an update escapes its transition, such as a setter called outside the callback or in a later timer.

for a senior

Show that you attribute the blocked frame before acting: is it render or commit, is it React at all, is the heavy subtree also on the urgent path — and then choose a structural fix rather than re-marking priorities.

for a principal

Own the standard that responsiveness is a budget enforced by bounding work per interaction — fewer committed nodes, computation out of render — with scheduling primitives as the last layer, not the first remedy anyone reaches for.

## Priority is not a speed-up The first thing to internalise is that marking an update as a transition does not make any component render faster and does not remove any work. It changes only *who gets the main thread first* and *whether the render may be abandoned*. If the total work per keystroke is too large, a transition redistributes the pain rather than removing it. ## Cause 1: the work cannot be sliced React's ability to stay responsive during a transition rests on yielding between units of work — roughly, between components as it walks the tree. It cannot pause in the middle of one component's function body. Two shapes defeat it: ```jsx // one component, one very long synchronous computation — indivisible function Report({ rows }) { const summary = computeExpensiveSummary(rows); // 180ms of straight-line JS return <pre>{summary}</pre>; } ``` and a component that returns an enormous number of children at once, where each unit is tiny but the total between yields is still large. In both cases a frame is missed while that work runs, regardless of priority. The answers are structural: make the expensive computation cheaper or move it out of render, and render fewer nodes (windowing a long list is the standard move) so React has somewhere to yield. ## Cause 2: the update escaped the transition Only updates *scheduled during* the `startTransition` callback are marked. Common escapes: ```jsx startTransition(() => { setTerm(next); }); setSort(next); // outside the callback — urgent, and it re-renders the same tree ``` ```jsx startTransition(() => { setTimeout(() => setTerm(next), 0); // runs after the callback returned — urgent }); ``` A related trap is forcing synchronous work around the update, which defeats the scheduler by design. And one urgent update that touches the same expensive subtree is enough to undo the whole arrangement — the transition-marked update is irrelevant if a sibling urgent update re-renders the same components in the same commit. Version matters for one specific case. In React 19, `startTransition` accepts an async function and updates scheduled after an `await` inside it are still part of the transition (with the pending state held until the function settles). In React 18 that was not the case: work after the await fell back to urgent priority, and codebases carried awkward nested-`startTransition` workarounds. If you are reading old code, that pattern is a migration artefact. ## Cause 3: the cost is not in the render at all Walk the whole keystroke, not just the setter: - **Work in the handler itself.** Parsing, sorting, or serialising before you call the setter runs synchronously in the event, at the highest possible priority, and blocks the keystroke. A transition cannot help with code that already ran. - **The urgent path renders the heavy subtree too.** If the expensive component sits inline in the same component as the input and cannot bail out on unchanged props, the urgent render walks it anyway. You then pay the cost twice — once urgently with stale data, once in the background with fresh data. - **Commit is synchronous.** React's commit phase — DOM mutation and layout effects — is never interruptible. A transition can be paused while rendering, but once it commits, thousands of DOM insertions plus any synchronous layout measurement run in one uninterruptible block. - **Something outside React.** A long task from an analytics script, a synchronous storage write, or a third-party widget occupies the same main thread and produces identical symptoms. ## Cause 4: too many transitions, not too few Abandoned renders are wasted work. Under sustained fast typing on a very expensive tree, React may start and discard render after render; the machine is busy the whole time. This is where combining strategies is legitimate: reduce how often the expensive update is even requested, and let the transition govern the priority of the ones that remain. ## Narrowing it down The useful discipline is to attribute the blocked frame before changing anything. Establish whether the janky interval is inside a React render at all, whether the update you marked is really being scheduled as a transition, and whether the expensive subtree is being rendered on the urgent path as well as the background one. Only then decide between shrinking the work, splitting the component so React has yield points, rendering fewer nodes, or moving the computation out of render entirely. ## The honest summary for an interview Transitions decouple a fast update from a slow one. They do not bound the cost of the slow one, they do not survive an update scheduled outside their callback, and they do nothing for the synchronous commit or for work that runs before React is involved. When a transition does not help, the answer is almost always that the work itself is too large or too monolithic.

  • Why does a single component that renders ten thousand rows defeat time slicing?
    React yields between units of work as it walks the tree, and it cannot pause inside one component's return value. A component that emits an enormous child set in one pass gives the scheduler a very long stretch with no yield point, so the frame is missed regardless of the update's priority.
  • You suspect the update is not actually being treated as a transition. What would you check first?
    Where the setter is called. Only updates scheduled while the startTransition callback is on the stack are marked, so a setter in a later timer or callback, or one sitting outside the callback next to it, is urgent. Also check whether a sibling urgent update re-renders the same subtree in the same commit.
  • If the transition renders fine but the commit still drops frames, what are you looking at?
    The commit phase, which is synchronous and cannot be interrupted at any priority. Thousands of DOM insertions plus layout effects that read geometry run in one block. The remedies are structural — commit fewer nodes, or defer measurement work — not a different priority marking.
  • Under sustained fast typing, can transitions make things worse?
    They can burn more total CPU, because each abandoned render is work already performed and thrown away. Responsiveness usually still improves, since the keyboard is never blocked, but on a very expensive tree it is reasonable to also reduce how often the expensive update is requested at all.

saying these in an interview costs you the question

  • Assumes a transition makes the render itself faster
  • Thinks React can pause inside one component's render
  • Forgets updates outside the callback stay urgent
  • Believes the commit phase is interruptible too
  • Reaches for another priority marking instead of less work

context