What makes a tap feel slow?
When you tap a button, the browser cannot react until the main thread is free. Then it runs your click handler. Then it draws the next frame so you can see the result. Interaction to Next Paint (INP) measures that whole wait, from the finger touching the screen to the next picture on it. Google calls 200 ms or less good and anything over 500 ms poor, measured at the 75th percentile of real visits.
The bakery's Add to order button scores about 536 ms. The handler recalculates the basket, prices and collection slots, which work(520) stands for, and only then changes the message. For over half a second the visitor sees nothing, so they tap again, or decide the site is broken.
INP has three parts, and the Vitals view shows each. Input delay is the time spent waiting for whatever the main thread was already doing. Processing is your handler's own work. Presentation is drawing the frame, a fixed 16 ms in the simulator. Here the delay is zero and almost all of the 536 ms is processing.
What is a long task?
A task is one uninterrupted stretch of work on the main thread: running a script, handling an event, running a setTimeout callback. A long task is one over 50 ms. Nothing else can happen while it runs. A tap that arrives a few milliseconds after a 280 ms task starts has to wait for the whole thing, and that wait becomes input delay.
The bakery page has one long task while it loads: the setTimeout callback that draws today's list, 280 ms in one piece. It counts towards Total Blocking Time (TBT), which adds up the part of each task over 50 ms. TBT is a lab number. It stands in for INP when nobody is tapping, and pages with a high TBT nearly always have poor INP for people who tap early.
How does `scheduler.yield()` help?
Calling await scheduler.yield() inside an async function ends the current task and asks the browser to carry on with the rest of the function in a new one, soon. In between, the browser can draw a frame and handle any taps waiting in line. The continuation goes to the front of the queue, ahead of ordinary timers, so the work still finishes promptly.
Splitting the 520 ms of basket work into thirteen pieces of 40 ms, with a yield between each, keeps every task under 50 ms. The total work is the same, slightly more in fact. What changes is that the phone is never stuck for more than 40 ms at a time.
Chrome has supported scheduler.yield() since version 129, and other browsers have been adding it since. For browsers without it, a common fallback wraps setTimeout in a promise. The simulator provides scheduler.yield() and a yieldToMain() alias.
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
}Why show the feedback before the slow work?
INP stops counting at the next paint after the tap. If the handler changes the message and then yields, that paint happens straight away and shows the change. Everything after the yield still runs, but the visitor already knows their tap worked.
Order matters. In the Break It page, the handler does its 260 ms save, then updates the message, then yields. The yield is there, but it comes too late to help: the paint waits for the save. Move the visible change to the top and the yield straight after it.
Feedback does not have to be the final result. "Adding…" with a spinner, a button that turns grey, or a count that goes up by one are all honest, quick responses. Then the real work follows in pieces, and the final state replaces the placeholder.
document.querySelector('#add').addEventListener('click', async function () {
document.querySelector('#status').textContent = 'Added: cinnamon buns.';
await scheduler.yield(); // the screen updates here
for (let i = 0; i < 13; i++) {
work(40); // one slice of the basket maths
await scheduler.yield();
}
});

How small should each piece be?
Under 50 ms is the line the long-task rule draws, and it is a sensible target. A tap that lands in the middle of a 40 ms piece waits at most 40 ms, which leaves plenty of room inside the 200 ms budget for the handler and the paint.
In real code you rarely know how long each step takes, so a common pattern is to check the clock: do items until 30 or 40 ms have passed, then yield. That adapts to slow phones automatically, doing fewer items per piece where each one takes longer.
Yielding has a small cost, so do not yield after every single line. Yield between meaningful chunks: after each batch of 50 list items, after each section of a report, after each image is processed.
let deadline = performance.now() + 40;
for (const item of items) {
drawItem(item);
if (performance.now() > deadline) {
await scheduler.yield();
deadline = performance.now() + 40;
}
}Where does the simulator differ from a real phone?
The simulator taps the button at 2 seconds, after the page has finished its loading work, so the input delay here is zero and the lesson focuses on the handler. A real visitor might tap at any moment, including during the 280 ms list task, which is why you split that task too.
Yielding behaves as it does in a real browser. If a tap lands during a run of yielded pieces, it waits only for the piece running at that moment, and the browser handles it before the next piece starts. That is why splitting the loading work keeps input delay small too.
The simulator also counts only the work before the first yield as processing, and adds a fixed 16 ms for drawing. On a real phone, style recalculation and layout after a big change to the page can add tens of milliseconds to that last part, so keep the first visible change small.
How do you find slow taps on a real site?
In Chrome DevTools, the Performance panel shows live INP for each interaction as you click around the page, with the input delay, processing and presentation split out. Record a trace while you tap, and long tasks appear with a red corner. Click one to see which function took the time.
Lab tests only see the taps you make. For what real visitors feel, PageSpeed Insights shows INP from the Chrome UX Report when a site has enough traffic. A page can pass every lab test and still have poor INP because real visitors tap during loading, on slower phones, or use a feature you never clicked.
What does AI-written code often get wrong here?
AI assistants often write handlers that do everything in one go: validate, recalculate, re-render a whole list, save, then update the button. It works, and on a fast laptop it feels fine. On a phone it becomes a half-second freeze. Look for the visible change in any handler it writes and check that it comes first.
When asked to fix slow interactions, AI tools sometimes wrap the slow code in setTimeout(…, 0) and call it done. That does let the first paint happen sooner, but the deferred code still runs as one long task, blocking the next tap. They also sometimes suggest requestIdleCallback for work the visitor is waiting to see, which can delay it for a long time on a busy page. Split the work and yield between the pieces, and measure INP before and after.
Field notes
- INP measures the wait from a tap until the next frame shows the result.
- A long task is any stretch of main thread work over 50 ms.
await scheduler.yield()lets the browser paint and handle taps between pieces.- Update what the visitor sees first, then yield, then do the slow work.
Easy to get wrong
INP counts the work before the first paint after the tap. A yield after the slow work does not help, because the visitor has already waited for it.
Moving a long task later lets the first paint happen, but the task is still 520 ms long and blocks the next tap or scroll. Split it.
await is only allowed inside an async function. Without it the whole script fails to run, and the button does nothing at all.
Questions people ask
Is INP the same as First Input Delay?
No. First Input Delay measured only the waiting part of the first tap. INP replaced it in 2024 and measures the whole response, including your handler and the paint, across all taps, key presses and clicks on the page.
Do scrolling and hovering count towards INP?
No. INP counts taps, clicks and key presses. Scrolling is usually handled away from the main thread, though a busy main thread can still make scroll-linked effects stutter.
Can I move the work off the main thread entirely?
For pure calculation, yes: a Web Worker runs code on another thread. Workers cannot touch the page directly, so they suit things like sorting or searching data, with the result sent back to the main thread to draw.
Does yielding make the work take longer?
Slightly, because the browser does other things in between. The visitor does not notice, since the screen has already responded and stays responsive throughout.