QuestionsWeb Performance

Long tasks and yielding to the main thread

Main-Thread / INPMediumWeb Performance

What is a 'long task', why does it wreck INP, and how do you break one up so the UI stays responsive?

What it tests

Understanding the single main thread, the 50ms long-task threshold, and yielding strategies.

Approach & answer

The browser runs your JS, styling, layout, paint, and event handling on ONE main thread. While a task holds that thread, nothing else can happen — a click can't be processed, a frame can't paint. A 'long task' is any task that occupies the main thread for more than 50ms; anything longer and the browser can't respond to input within that window, so a tap that lands during a long task waits for it to finish. That waiting IS the input-delay portion of INP, so a few long tasks (a big JSON parse, an expensive render, a heavy loop) are the classic cause of poor responsiveness even on an otherwise 'fast' page. The fix is to break long work into small chunks and YIELD between them, letting the browser process pending input and paint. Options, roughly in order: (1) scheduler.yield() — the modern primitive; await it to yield and continue after the browser handles higher-priority work. (2) A setTimeout(0) / MessageChannel yield as a fallback. (3) requestIdleCallback for truly non-urgent work, which runs only when the thread is idle. (4) Move the work off-thread entirely to a Web Worker when it's pure computation. The mental model: prefer many short tasks over one long one, and yield right after handling input so the interaction paints quickly, then continue the heavy work. Frameworks add their own scheduling (React's concurrent renderer time-slices for the same reason). Measure long tasks with the Long Tasks API / PerformanceObserver, and target keeping tasks well under 50ms.

Use this technique when

Fixing poor INP / janky interactions caused by heavy synchronous JS on the main thread.

Code

// A long loop blocks the main thread -> input can't be handled -> bad INP
async function processAll(items) {
  for (let i = 0; i < items.length; i++) {
    doExpensiveWork(items[i]);

    // Yield periodically so the browser can paint & handle clicks
    if (i % 100 === 0) {
      if ('scheduler' in window && scheduler.yield) {
        await scheduler.yield();                       // modern
      } else {
        await new Promise((r) => setTimeout(r, 0));    // fallback
      }
    }
  }
}

// Detect long tasks in the field
new PerformanceObserver((list) => {
  for (const t of list.getEntries()) {
    if (t.duration > 50) console.warn('long task', t.duration.toFixed(0), 'ms');
  }
}).observe({ type: 'longtask', buffered: true });

References