Break up long tasks to keep a page responsive

A long task freezes the page for 50 milliseconds or more. How to find them, and how to split them with yielding, time-boxed loops, priorities and workers.

IntermediateHow-to7 min readUpdated October 30, 2026

In this guide
  1. What a task is, and why 50 milliseconds
  2. Step 1: find the long tasks
  3. Step 2: choose a technique
  4. Technique 1: yield between steps
  5. Technique 2: process a big list in time-boxed chunks
  6. Technique 3: give work a priority
  7. Technique 4: do less in the first place
  8. Technique 5: move the work off the main thread
  9. Mistakes to avoid
  10. Check that it worked
  11. Check your own page
  12. Sources

What a task is, and why 50 milliseconds

A task is a discrete piece of work the browser does: running a script, parsing HTML, calculating styles, drawing. The main thread is where most tasks run, and almost all the JavaScript you write works there. It can process only one task at a time.

web.dev defines a long task as one that stretches beyond 50 milliseconds. If the visitor taps or types while a long task runs, or if an important update to the screen is due, the browser is delayed in handling it. That delay is the input delay part of Interaction to Next Paint. In a lab test, the portion of each long task above 50 milliseconds is added up as Total Blocking Time. Breaking up a long task therefore helps the metric you see in the field and the one in the lab.

JavaScript uses a run-to-completion model: a task runs until it finishes, however long that takes. So a function like this one is a single long task, however neatly it is organized:

function saveSettings() {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}

Splitting code into functions makes it readable. It does not make the browser's work any shorter. To let the browser breathe, you have to hand control back to it between the pieces. That is called yielding to the main thread.

Step 1: find the long tasks

In Lighthouse. The diagnostic called Avoid long main-thread tasks lists the longest tasks, up to 20, that ran for 50 milliseconds or more at the top level, with the script that caused each one. Lighthouse describes the list as useful for finding the worst contributors to input delay.

In Chrome DevTools. Record a load, or record yourself doing the slow thing, in the Performance panel. Long tasks are marked with a red triangle in the corner, and the part beyond 50 milliseconds is shaded with red stripes. Click a task and look at the Bottom-Up tab, grouped by activity, to see what took the time inside it. Often it is one loop, one library call or one forced layout.

In the field. The Long Animation Frames interface reports the frames that blocked the main thread, with details about the scripts that ran in them. At the time of writing it works in Chrome and Edge, and it is still marked experimental. The INP guide explains how to find the slow interactions of real visitors.

Step 2: choose a technique

SituationWhat to use
A handler does a visible update and a lot of other workDo the visible part first, yield, then do the rest
A loop over thousands of itemsProcess in time-boxed chunks, yielding between them
Work nobody is waiting for, such as analyticsRun it at a low priority
Heavy computing that does not touch the pageMove it to a web worker
Many widgets starting up at loadStart each one when it is needed
A third-party tag that fires earlyDelay it. See third-party scripts

Technique 1: yield between steps

Yielding means ending your turn so that other work can run, then continuing. The modern way is scheduler.yield(), which pauses your function and resumes it as a prioritized task. MDN says the continuation defaults to the user-visible priority. It works in Chrome 129 and later and in Firefox 142 and later, and not in Safari. A small helper adds a fallback:

const yieldToMain = () =>
  globalThis.scheduler?.yield
    ? scheduler.yield()
    : new Promise((resolve) => setTimeout(resolve, 0));

async function saveSettings() {
  showSpinner();             // visible: do it now
  updateUI();
  await yieldToMain();       // let the browser paint and handle input
  saveToDatabase();          // not visible: can wait a turn
  await yieldToMain();
  sendAnalytics();
}

A few points about this pattern:

Technique 2: process a big list in time-boxed chunks

A loop over a large array may run quickly for each item and still be long in total. Count time, not items, because items differ in cost and devices differ in speed:

async function processItems(items, handleItem) {
  let deadline = performance.now() + 40;   // stay well under 50 ms
  for (const item of items) {
    handleItem(item);
    if (performance.now() > deadline) {
      await yieldToMain();
      deadline = performance.now() + 40;
    }
  }
}

Each chunk is a short task, and the browser can respond to input between chunks. Show progress if the job takes long, so the visitor knows something is happening. The cost is that the whole job takes a little longer, because other work now takes turns with it.

Technique 3: give work a priority

The Scheduler API lets you say how urgent a job is:

if (globalThis.scheduler?.postTask) {
  scheduler.postTask(sendAnalytics, { priority: 'background' });
} else {
  setTimeout(sendAnalytics, 0);
}

postTask() takes three priorities: user-blocking for critical work, user-visible as the default, and background for the lowest. It works in Chrome 94 and later and Firefox 142 and later, not in Safari, so keep a fallback.

Two older tools deserve a warning:

Technique 4: do less in the first place

Breaking up work spreads it out, but removing it is better.

Technique 5: move the work off the main thread

A web worker runs JavaScript on another thread, so a heavy job cannot block taps. A worker has no access to the page itself, so it suits pure computation: parsing a large file, searching, sorting, image processing or encryption. It sends results back with messages. The cost is the work of passing data between the threads, so it pays off for jobs that take real time. The full pattern, with the limits and a small library that makes messages easier, is in web workers: moving heavy work off the main thread.

Mistakes to avoid

Check that it worked

  1. Record the same action in the Performance panel again. The long task should now be several short ones, with gaps between them.
  2. Run the lab test and check that Total Blocking Time went down.
  3. Wait for the field data. Interaction to Next Paint is judged over 28 days of real use. See the INP guide.

Check your own page

Run the free audit and look at the Avoid long main-thread tasks list and the Total Blocking Time. If most long tasks belong to scripts from other companies, start with third-party scripts. If they belong to your own code, start with the longest one on the list.

Sources

Start here

Free website audit

Live Google data for any public page: mobile and desktop.

Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it

Rather have this done for you? Speed optimization · Book a call

Related guides

All guides →