Interaction Responsiveness
A slow tap is three separable delays: waiting for the thread, running the handler, and producing the frame that shows the result. Each has a different fix.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
When a tap or a keystroke feels slow, which of the three phases between the input and the pixel is actually slow?
Someone presses a button and expects the interface to acknowledge it immediately — a pressed state, a spinner, a changed value. What they are judging is not the work; it is the acknowledgement.
Time the click handler. If it returns quickly, the interaction is fast, and any remaining slowness must be the network.
The handler can return in a fraction of a millisecond and the user can still wait a visible beat, because the state change it queued has not been rendered yet.
- The handler can return in a fraction of a millisecond and the user can still wait a visible beat, because the state change it queued has not been rendered yet.
- The handler may not have started when the user pressed. If a task was already running on the main thread, the event sat in a queue until that task finished, and none of that waiting appears in the handler's own timing (The Event Loop, Precisely).
- A
keydownhandler that returns quickly but blocks the default action can still make typing feel laggy, because the character does not appear until the next frame after the handler chain completes (Keyboard Events). - The slow interaction is often not the one you instrumented. It is the third tap, after a data refresh has queued a re-render of a large list (List Virtualization).
- On a mid-range phone the same handler runs several times slower, and the render that follows it costs several times more, so a decomposition that looks balanced on a laptop is dominated by one phase in the field (The Real Cost of JavaScript).
What is actually happening
In the browser, not in the framework.
- An interaction spans three phases. Input delay: the time between the user's input and the moment your handler starts. The thread was busy; the event waited.
- Processing duration: the time your handler and every other handler for that event spend running, plus whatever synchronous work they trigger.
- Presentation delay: the time between the last handler finishing and the browser painting the frame that reflects the change — style, layout, paint, composite, plus anything else that got queued in front of it.
- This decomposition is the useful part, because the three phases have three unrelated fixes. Input delay is fixed by making other work smaller or yielding more often; processing is fixed by doing less in the handler; presentation is fixed by changing less, or changing cheaper things (The Cost of a Change).
- The browser produces a frame only at a rendering opportunity, which happens between tasks, never in the middle of one. A handler that runs long does not get interrupted to show a spinner it just added to the DOM (The Rendering Opportunity).
- The field metric for this concern is interaction to next paint, and it measures the whole span rather than the handler. That is deliberate: it is the only definition that matches what the user experiences, and it is why a fast handler can still produce a bad measurement.
- The metric reports the worst interactions of a page visit, not the average of them. One badly-behaved interaction in a session is the thing a user remembers, and the metric is built to agree with them.
What this makes the browser do
And which of it is avoidable.
- Hit testing: for a pointer event the browser must determine which element was hit, which can require up-to-date layout. A pending layout invalidation makes the very first step of handling a click more expensive (Layout Thrashing).
- Dispatch: capture, target and bubble phases, running every matching listener along the path. Delegation makes this cheaper to attach and slightly more expensive to dispatch (How an Event Is Dispatched).
- Default actions: scrolling, focus movement, checkbox toggling, form submission. A non-passive listener forces the browser to wait for your handler before it can scroll, which is why scroll feels stuck (Passive Listeners).
- Rendering after the change: style recalculation for the invalidated subtree, layout if geometry could have changed, paint, composite. Avoidable work here is anything that invalidates more of the tree than the change requires (Style Invalidation).
- Unavoidable: at least one frame of latency. There is no way to show a change before the next rendering opportunity.
Three phases, three different fixes
The single most useful thing you can do with a slow interaction is refuse to treat it as one number. Split it into waiting, working and showing, and the fix follows from which part is large. Teams that skip this step optimise the handler — because that is the code they own — and are surprised when nothing changes.
Note where the phases end. The interaction is not over when your code returns; it is over when the user can see that it happened. Everything between those two moments is the presentation phase, and it is where framework re-render cost, layout and paint all live.
- Input delay — the thread was busy. Fix by making other work smaller, later, or interruptible.
- Processing — your handlers. Fix by doing less synchronously, or doing it elsewhere.
- Presentation — the frame that shows the change. Fix by changing less, or changing cheaper things.
- A handler timing measures exactly one of the three, and usually not the largest.
The same tap, three ways to be slow
Below is one interaction drawn three times in relative units. The total span is similar in each; the shape is completely different, and so is the fix. Reading the shape is the skill — the absolute numbers would be a property of one device and would not transfer.
The trap is that the second shape is the only one a handler timing can see. If your instrumentation wraps the handler, the first and third cases both report "fast" while the user waits.
- A: waiting — a long task is running — Input delay dominates. The handler is innocent; something else owned the thread. Fix: break that task up or move it.
- B: handler does the work synchronously — Processing dominates. Fix: acknowledge first, then do the work in a later task or a worker.
- C: handler returns immediately — This is the only bar a handler timing reports. It says everything is fine.
- C: re-render of a large subtree — Presentation dominates. Fix: render less — narrower state, containment, or a windowed list.
Same total, three causes, three fixes. The decomposition is what makes this a diagnosis instead of a guess.
Which phase is it?
Once the split is available, the diagnosis is mechanical. The table below is the working version of that: a symptom, the phase it points at, the underlying cause, and the class of fix. It is worth reading the response column as a set of options rather than instructions — each has a cost, covered in this lesson's trade-offs.
One recurring surprise: a great many "slow interaction" tickets turn out to be input delay caused by code nobody on the team wrote, arriving through a tag manager.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| First interaction after load feels stuck | A visible pause before anything happens, only early in the session | Input delay: hydration and third-party scripts are still executing on the main thread | Defer non-critical script, split hydration, and audit what runs before first interaction (Islands and Partial Hydration). |
| Every tap has the same small hesitation | Consistently sluggish rather than occasionally frozen | Processing: the handler does work — validation, formatting, deriving — that could happen elsewhere | Acknowledge synchronously, move the rest to a later task; hoist derivation out of the handler (Derived State). |
| The handler is instant, the screen is not | Instrumentation says fast, users say slow | Presentation: the state change re-renders far more of the tree than it needed to | Narrow state ownership, contain the subtree, or window the list (Who Owns This State?, CSS Containment). |
| Typing lags behind the keyboard | The caret trails the user by a word | Every keystroke triggers a full re-render, a filter over a large array, or both | Keep the input value cheap and immediate; defer the expensive consequence, never the character (Form State Is a Draft). |
| Scroll feels stuck when touched | The page hesitates before it begins to move | A non-passive touch or wheel listener: the browser must wait to see whether you cancel | Mark the listener passive, and remove layout reads from it (Passive Listeners). |
| Interaction is fine alone, terrible during a refresh | Slowness correlates with background data updates | Input delay from a re-render triggered by arriving data | Render arriving data in interruptible chunks, or off the interaction path entirely (Stale-While-Revalidate). |
How to build it
Most important first.
- Acknowledge immediately, then do the work. Set the pressed or busy state, let a frame paint, and only then run the expensive part. The perceived interaction ends at the acknowledgement (Optimistic UI).
- Keep handlers short by moving everything that is not needed for the acknowledgement out of them — into a scheduled callback, an idle callback, or a worker (Yielding and Scheduling).
- Break long tasks up so an event that arrives mid-work waits for a slice rather than the whole job. Yielding does not make the work faster; it makes the thread reachable (Long Tasks).
- Move genuinely CPU-bound work off the main thread. Parsing a large response, diffing a big structure, doing image work — none of that needs the DOM (When a Worker Is Actually the Answer).
- Reduce what the update invalidates. A change scoped to one subtree costs style and layout for that subtree; a change to a class on the body costs the document (CSS Containment).
- Make listeners on scroll and touch passive unless you genuinely need to cancel the default, and never do layout reads inside them (Scroll and Input Latency).
- Debounce the expensive consequence of typing, not the visible feedback. The character must appear on every keystroke; the search request need not (Controlled vs Uncontrolled Inputs).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Keyboard interactions go through the same three phases and usually more handlers: focus movement, roving tabindex updates, live-region writes. A component that is responsive to a click can be sluggish to a keypress (Keyboard Operability).
- A busy state must be announced, not only drawn.
aria-busy, a live region, or a disabled control with an accessible name gives a screen-reader user the acknowledgement a pressed state gives everyone else (Live Regions and Announcement). - A long task blocks the accessibility tree update along with everything else, so announcements arrive late and out of context. The screen-reader experience of jank is silence followed by a burst.
- Never disable a control without moving focus somewhere sensible. A disabled element loses focus, and a keyboard user is dropped to the top of the document at the exact moment they were waiting for a result (Focus Management).
- Respect reduced-motion preferences in the acknowledgement. A transition that communicates progress must have a non-animated equivalent, or the acknowledgement disappears for the users who opted out (Contrast, Colour and Motion).
What can go wrong
- Yielding in a way that never actually yields. Awaiting an already-resolved promise runs a microtask, which happens before the rendering opportunity, so the thread is not released and no frame is produced (The Microtask Checkpoint).
- Debouncing the input value itself, which makes the caret lag behind the user's typing — the most visible responsiveness bug there is.
- Moving work to a worker and then transferring so much data across the boundary that the structured clone costs more than the work saved (Structured Clone and Transferables).
- Adding a spinner inside the same task as the blocking work, so the spinner and the result appear in the same frame and the user sees no feedback at all.
- Optimising the handler you can see while the presentation delay — a re-render of the whole page triggered by a context value — remains the dominant phase (What a Component Costs to Render).
- A
requestAnimationFramecallback that does the heavy work, which moves the cost into the frame instead of removing it and produces a dropped frame rather than a delayed one (The Frame Budget).
- A user can interact again before the first interaction has painted. Without a request key or a cancellation, the second response can overwrite the first in the wrong order (Out-of-Order Responses).
- An optimistic update races the server response, and a rejection can arrive after the user has moved on to another screen (Optimistic UI).
- Events queued during a long task all arrive at once when it ends, so a user who tapped three times gets three handlers in the same burst.
- Optimistic acknowledgement is a UI affordance and never an authorization decision. Showing the row as deleted before the server agrees is fine; treating it as deleted is not (What the Frontend Is Responsible For in Auth).
- A disabled button is a hint, not a control. The underlying request can still be issued from the console, so the server must enforce the rule that the disabled state was expressing (Authorization-Aware UI).
- Rapid repeated interactions produce duplicate requests. Idempotency belongs to the API contract, and the client's job is to not make the problem worse (Idempotency Keys: The Mechanism in API Design).
- Handlers that echo user input into the DOM during an interaction are a live XSS surface; the speed of the path does not change the sink (Cross-Site Scripting).
- "My handler takes under a millisecond, so the interaction is fast." The handler is one of three phases, and it is frequently the smallest of them.
- "It is the network." The network cannot explain a delay before the request was made. Check the input delay before blaming the API.
- "
requestAnimationFramemakes it faster." It schedules work for just before the next frame. Heavy work there is heavy work inside the frame budget, which drops the frame instead of delaying it (The Frame Budget). - "Awaiting a promise yields to the browser." It yields to the microtask queue, which is drained before rendering. The page is still frozen (The Microtask Checkpoint).
- "We measure the average interaction." Users remember the worst one, and the field metric is deliberately built around that rather than around the mean.
- "Debounce fixes typing lag." Debouncing the search is right; debouncing the displayed value is the cause of typing lag, not the cure.
Measuring it, and what changes in the field
- Field data for the responsiveness concern, broken down by the three phases where the tooling supports it — the split is what tells you which fix to apply (Vitals in the Field).
- The Performance panel's interaction track, which shows the input, the handler and the frame that followed as one span (A Mental Model of the Devtools).
- Long-task and long-animation-frame records, attributed to a script URL, which is how you find out that the input delay belongs to a third-party tag (Long Tasks).
- Event timing entries collected in the field, which give you the same decomposition for real users on real devices (Real User Monitoring).
- For the presentation phase specifically, the rendering breakdown of the frame after the handler: style, layout, paint, composite (The Rendering Pipeline).
- On a slow device every phase grows, but not equally: processing and presentation scale with CPU while input delay scales with whatever else the page is doing, so the dominant phase can change between device classes (The Real Cost of JavaScript).
- On a slow network the interaction still needs to acknowledge immediately. Network latency belongs after the acknowledgement, never in front of it (Loading, Error, Empty — The States You Did Not Render).
- With a large list on screen, presentation delay dominates: the handler is trivial and the re-render is not (List Virtualization).
- Early in the page's life, input delay dominates, because hydration and third-party scripts are competing for the same thread the user just tapped on (Hydration).
- In a long-lived tab, accumulated listeners and subscriptions can make a single click run dozens of handlers that were supposed to have been torn down (Memory Leaks).
- Yielding more often makes the thread reachable and makes the total work take longer, because each yield costs a scheduling round trip and gives the browser a chance to do other things first.
- Optimistic acknowledgement makes interactions feel instant and creates a reconciliation problem when the server disagrees. That rollback path is real work and real UI (Rollback and Reconciliation).
- Moving work to a worker removes it from the main thread and adds a message boundary, a serialisation cost and a second copy of some state to keep consistent (Talking to a Worker).
- Splitting a render into chunks improves responsiveness during the update and can make the total update visibly slower, which is the right trade for a person and the wrong one for a benchmark.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALThe three-phase decomposition follows from the event loop: an event cannot be dispatched while a task is running, and a frame cannot be produced in the middle of one. That is true of every browser regardless of how it names or exposes the phases.
- SPEC-EVOLVINGThe responsiveness metric named here replaced an earlier one that measured only the delay before the first handler, precisely because that earlier definition missed most of what users feel. Its successor may be revised again, and its rating boundaries are published by the web vitals working group rather than fixed by the platform.
- BROWSER-SPECIFICThe per-phase breakdown is exposed most directly by Chromium, through its Interactions track and long-animation-frame records. Firefox and Safari expose event timing but do not present the same ready-made decomposition, so the same analysis there is assembled by hand.
- DEVICE-SPECIFICWhich phase dominates changes with the device: on a desktop with a fast CPU the presentation phase is often negligible and input delay comes from third-party script, while on a mid-range phone the same interaction is dominated by processing and rendering that the desktop trace barely showed.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Programming Languages & Runtime Internals — why the same handler runs several times slower before the engine has had a chance to optimise it, and what a deoptimisation costs mid-interaction.