Skip to main content
Performance

What Is a Long Task and Why It Slows Your Web App

How long tasks block the browser's main thread, cause jank, and hurt your Interaction to Next Paint score, plus how to break them up.

Updated

What Is a Long Task and Why It Slows Your Web App

You click a button. Nothing happens for 200ms. Then everything updates at once. That delay is a long task blocking the main thread.

Chrome DevTools flags it in the Performance panel:

Long task: 247ms (exceeded 50ms threshold)

A long task is any JavaScript execution that takes longer than 50 milliseconds without yielding control back to the browser. During that time, the browser cannot respond to user input, update the screen, or run animations. The page feels frozen.

Why 50 milliseconds

Humans perceive delays above 100ms as lag. The browser needs some of that 100ms budget for its own work (style calculation, layout, paint). That leaves roughly 50ms for JavaScript per frame. Anything longer and the user notices.

The Long Tasks API formalizes this threshold. A “long task” is specifically a task that occupies the main thread for more than 50ms. Not a suggestion. A hard number defined in the spec.

How long tasks hurt your app

Input delay

While a long task runs, the browser queues all user events (clicks, keystrokes, scrolls). The event handlers don’t fire until the task finishes. If a user clicks a button during a 300ms task, they wait 300ms before anything happens.

This directly impacts Interaction to Next Paint (INP), one of Google’s Core Web Vitals. INP measures the worst-case delay between a user interaction and the next visual update. Long tasks are the primary cause of poor INP scores.

Animation jank

The browser targets 60 frames per second, which means a new frame every 16.6ms. A 200ms long task means the browser skips 12 frames. Animations freeze. Scrolling stutters. CSS transitions jump to their end state instead of animating smoothly.

Delayed page load

Long tasks during page load delay the DOMContentLoaded event, block rendering of below-the-fold content, and push back Time to Interactive. A page can be fully loaded (all resources downloaded) but still unusable because JavaScript is executing a massive initialization task.

Finding long tasks

Performance panel in DevTools

Record a trace in the Performance panel. Long tasks appear as red-cornered blocks on the Main thread row. Click one to see the call stack. The bottom of the stack (where the most time is spent) is where you need to optimize.

Long Tasks API

Detect long tasks programmatically with a PerformanceObserver:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(
      'Long task detected:',
      entry.duration.toFixed(1) + 'ms',
      entry.name
    )
  }
})

observer.observe({ type: 'longtask', buffered: true })

This reports every task over 50ms. In production, send these to your analytics to understand what your real users experience:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    navigator.sendBeacon('/analytics/longtask', JSON.stringify({
      duration: entry.duration,
      startTime: entry.startTime,
      url: location.href,
    }))
  }
})

observer.observe({ type: 'longtask', buffered: true })

Common sources of long tasks

Large list rendering. Rendering 1,000 DOM nodes at once blocks the thread for hundreds of milliseconds. Virtualize instead.

JSON parsing of large payloads. JSON.parse() on a 2MB response runs synchronously. There’s no async alternative in the main thread.

Third-party scripts. Analytics, ad networks, chat widgets, and A/B testing tools regularly produce long tasks. You have limited control over these.

Bundle evaluation. When the browser downloads a 500KB JavaScript bundle, it must parse and evaluate all of it before executing any of it. That evaluation is a long task.

Complex DOM manipulation. Batch DOM writes that trigger expensive layout recalculations. Reading layout properties (like offsetHeight) between writes forces synchronous layout (layout thrashing).

How to break up long tasks

1. Yield with scheduler.yield()

The scheduler.yield() API (available in Chrome 115+) is the cleanest way to break a long task. It pauses your code, lets the browser process pending events and rendering, then resumes.

async function processItems(items) {
  for (const item of items) {
    processItem(item)

    // Yield every iteration to keep the main thread responsive
    if (navigator.scheduling?.isInputPending?.() || true) {
      await scheduler.yield()
    }
  }
}

Unlike setTimeout, scheduler.yield() preserves your task’s priority. The continuation runs at the same priority as the original task, not at the back of the queue.

2. Break work into chunks with setTimeout

The classic approach. Split a large task into smaller chunks and schedule each one with setTimeout(fn, 0):

function processInChunks(items, chunkSize = 50) {
  let index = 0

  function processChunk() {
    const end = Math.min(index + chunkSize, items.length)

    while (index < end) {
      processItem(items[index])
      index++
    }

    if (index < items.length) {
      setTimeout(processChunk, 0)
    }
  }

  processChunk()
}

Each chunk takes well under 50ms. Between chunks, the browser handles input and rendering.

3. Use requestIdleCallback for non-urgent work

requestIdleCallback runs your code when the browser is idle, with a deadline so you know how much time you have:

function processWhenIdle(items) {
  let index = 0

  function processChunk(deadline) {
    while (index < items.length && deadline.timeRemaining() > 5) {
      processItem(items[index])
      index++
    }

    if (index < items.length) {
      requestIdleCallback(processChunk)
    }
  }

  requestIdleCallback(processChunk)
}

This is ideal for non-visible work: prefetching data, pre-computing caches, or syncing analytics. Don’t use it for work that affects what the user sees right now.

4. Move computation to a Web Worker

Web Workers run on a separate thread. They can’t access the DOM, but they can handle data processing, sorting, filtering, and calculation without blocking the main thread.

// worker.js
self.onmessage = (e) => {
  const result = expensiveComputation(e.data)
  self.postMessage(result)
}

// main.js
const worker = new Worker('/worker.js')

worker.postMessage(largeDataset)
worker.onmessage = (e) => {
  renderResults(e.data)
}

For CPU-heavy work (image processing, large dataset sorting, search indexing), Workers are the right answer. The main thread stays completely free.

5. Code split and lazy load

If bundle evaluation is causing long tasks at page load, split your code so only the critical path loads immediately:

// Instead of importing everything upfront
const DashboardCharts = lazy(() => import('./DashboardCharts'))

// The chart code loads and evaluates only when needed

Combined with route-based splitting, this ensures the browser only evaluates the JavaScript needed for the current page.

Measuring improvement

After optimizing, verify that long tasks are reduced. The Total Blocking Time (TBT) metric in Lighthouse sums up all long task durations during page load. Lower TBT means fewer and shorter long tasks.

For runtime interactions, check INP in Chrome’s Web Vitals extension or in your field data (CrUX report). INP captures the actual delay users experience from long tasks during interaction.

Hushbug detects long tasks and performance bottlenecks automatically while you browse. Coming soon to the Chrome Web Store.