# Long tasks blocking the main thread

> Long tasks freeze the page and wreck INP. Break up heavy work, defer it, or move it to a worker so the main thread stays responsive to input.

_Category: Performance · Detector `long-task-attribution` · Severity: warning_

The browser runs your JavaScript on the same single thread it uses to respond to clicks and paint frames, so any task that runs too long freezes everything — the page can’t respond to input while it works. This is what wrecks INP (Interaction to Next Paint) and makes a page feel janky right after load. AI code tends to do expensive work synchronously in render or on mount instead of chunking or deferring it.

## Symptoms

- The page is unresponsive for a beat after it loads or on first interaction
- Clicks and typing feel delayed (poor INP)
- The Performance panel shows long yellow "Task" blocks with a red corner
- A single script dominates the main-thread flame chart

## How VibeCheck detects it

The `long-task-attribution` detector flags this live in the browser and reports it to the widget's Problems list — and to your coding agent over MCP.

- **Issue string:** `Script causing long frames: bundle.js`
- **Threshold:** A single script attributed to more than 3 long animation frames (LoAF)

## Root causes

- Expensive computation (parsing, sorting, formatting) run synchronously on the main thread
- Large hydration or initialization work on mount
- A heavy third-party script executing during load
- Rendering or diffing a very large component tree in one pass

## The fix

Keep individual tasks short. Break long work into chunks that yield to the browser between them, move pure computation into a Web Worker, defer non-critical work until the page is idle, and memoize expensive results so they don’t recompute on every render.

### Steps

1. Profile to find the script behind the long task (VibeCheck names it)
2. Move heavy pure computation into a Web Worker, or chunk it with yielding
3. Defer non-critical work with `requestIdleCallback` / `scheduler.yield`

_Yield to the browser between chunks so input can be handled_

```js
async function processInChunks(items) {
  for (let i = 0; i < items.length; i++) {
    doWork(items[i])
    if (i % 50 === 0) {
      // let the browser paint and handle input
      await (window.scheduler?.yield?.() ?? new Promise((r) => setTimeout(r)))
    }
  }
}
```

## Framework-specific fixes

### React

Wrap non-urgent state updates in `startTransition` so React can interrupt them for input, and memoize expensive derived values so they don’t recompute every render. Code-split heavy components with `React.lazy`.

```tsx
import { startTransition, useMemo, lazy } from 'react'

const Heavy = lazy(() => import('./Heavy'))
const rows = useMemo(() => expensiveSort(data), [data])

startTransition(() => setFilter(next)) // keeps typing responsive
```

[React docs](https://react.dev/reference/react/startTransition)

### Next.js

Move work off the client entirely: do the heavy computation in a Server Component or Server Action, and `next/dynamic`-import client-only heavy widgets so they don’t block first interaction.

```tsx
import dynamic from 'next/dynamic'
const Chart = dynamic(() => import('./Chart'), { ssr: false, loading: () => <Skeleton /> })
```

### Vanilla JS

Offload pure computation to a Web Worker so the main thread stays free for input and paint.

```js
const worker = new Worker(new URL('./crunch.js', import.meta.url), { type: 'module' })
worker.postMessage(data)
worker.onmessage = (e) => render(e.data)
```

## FAQ

### What counts as a long task?

The browser flags any task that occupies the main thread for 50ms or more. VibeCheck goes further and attributes long animation frames back to the specific script responsible, so you know what to fix.

### When should I use a Web Worker vs chunking?

Use a Worker for pure, CPU-bound computation with no DOM access (parsing, crunching, transforming data). Use chunking/yielding when the work must touch the DOM or React state and can be spread across frames.

### How does this relate to INP?

INP measures how long the page takes to visually respond to an interaction. Long tasks are the main cause of poor INP — while a long task runs, the browser can’t process the click or paint the response.

## Related problems

- [Large JavaScript bundles](https://vibecheck.wcgw.fun/fix/large-javascript-bundles.md) — Performance
- [Heavy dependencies](https://vibecheck.wcgw.fun/fix/heavy-dependencies.md) — Performance
- [Excessive DOM size](https://vibecheck.wcgw.fun/fix/excessive-dom-size.md) — Performance
- [Cumulative layout shift (CLS)](https://vibecheck.wcgw.fun/fix/cumulative-layout-shift.md) — Performance

---

Fix guide from VibeCheck — https://vibecheck.wcgw.fun/fix/long-tasks. Full site index for LLMs: https://vibecheck.wcgw.fun/llms.txt
