A long task is any single, uninterrupted piece of work on the browser's main thread that runs for more than 50 milliseconds. While a long task is running, the page cannot respond to clicks, taps, or key presses, and no new frames can paint. Long tasks are the primitive that produces bad Total Blocking Time (TBT) and slow Interaction to Next Paint (INP) — the latter being a Core Web Vital and a confirmed Google ranking signal.
(task_duration − 50ms) for every long task between FCP and TTI. Good TBT is ≤ 200 ms.Think of the main thread as a single-lane checkout. A 50 ms task is a customer with a basket; a 500 ms task is someone counting out coins. Everyone behind them — clicks, taps, animations — waits in the same line.
Long tasks cause the most viscerally bad parts of a slow site:
The browser exposes long tasks through the Long Tasks API, which fires a PerformanceObserverentry for any task whose execution exceeds 50 ms. The 50 ms threshold is not arbitrary — it comes from Google's RAIL response-time model:
// Detect long tasks in any browser that supports the API
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(
`Long task: ${entry.duration.toFixed(0)}ms`,
entry.name,
entry.attribution
);
}
});
observer.observe({ type: 'longtask', buffered: true });
}Total Blocking Time = Σ(task_duration − 50ms) for every long task between First Contentful Paint and Time to Interactive. A single 250 ms long task contributes 200 ms of TBT — instantly failing the "good" threshold by itself.
INP measures the worst real-user input response across a session. The interaction's total duration is the sum of input delay, processing time, and presentation delay — and a long task in any of those phases lengthens INP. Fixing long tasks during clicks and keypresses is the single most effective INP fix.
PerformanceObserver to your real-user monitoring so you can see long tasks on real devices, not just simulated ones.scheduler.yield()The native way to break up work in 2026. Inside a long loop or pipeline, awaiting scheduler.yield() hands control back to the browser — letting it process pending input and paint a frame — and resumes your work as the next task in the queue.
async function processItems(items) {
for (const item of items) {
doWork(item);
// Yield so the browser can handle clicks and paint
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
await new Promise(r => setTimeout(r, 0));
}
}
}Anything that doesn't touch the DOM and runs longer than 50 ms — JSON parsing of large payloads, search/filter over big datasets, image manipulation, crypto — belongs on a worker thread. The main thread stays free to respond to input and paint frames.
// main.js
const worker = new Worker(
new URL('./filter.worker.js', import.meta.url),
{ type: 'module' }
);
worker.postMessage({ query, dataset });
worker.onmessage = (e) => render(e.data);Rendering 5,000 rows is a guaranteed long task. List virtualization renders only the rows currently in the viewport (plus a small buffer), so the DOM stays small and updates stay fast. Most frameworks have a virtualization library — use one before you ship a long list.
Scroll, resize, and input events fire dozens of times per second. If their handler does anything non-trivial, you get cascading long tasks. Debounce handlers that should run after the user pauses (search-as-you-type with a 200 ms debounce) and throttle ones that should run periodically during continuous events (scroll-driven layout updates at 16 ms).
Full-page hydration on load is one of the most common sources of multi-hundred-millisecond long tasks. Move to islands or component-level hydration so only the genuinely interactive components run on the client, and so below-the-fold widgets hydrate on idle or on intersection.
A render that reads from a Map 5,000 times, or recomputes the same selector inside a loop, can quietly create 100+ ms tasks. Memoize expensive selectors, hoist invariant work out of loops, and prefer batch updates over per-item reflows.
requestIdleCallback for Background WorkLogging, prefetching, analytics buffering, and other non-urgent work should run when the main thread has nothing else to do. requestIdleCallbackguarantees your work won't collide with a user interaction.
requestIdleCallback(() => {
flushAnalyticsQueue();
prefetchNextRoute();
}, { timeout: 2000 });The click handler is the hottest path in the app. Anything heavy inside it inflates INP directly. Update minimal state synchronously to give immediate feedback, then schedule the rest with scheduler.postTask, requestIdleCallback, or a worker.
What's happening: Each keystroke runs a synchronous filter over a 10,000-row dataset on the main thread.
Fix: Debounce the input (200–300 ms) and run the filter in a Web Worker. Render virtualized rows. Typical result: from 400 ms long tasks per keystroke to none.
What's happening: The framework hydrates the entire page on load, producing a single 600 ms long task that destroys INP for any click in the first few seconds.
Fix: Adopt islands/partial hydration. Mark static sections non-interactive. Hydrate below-the-fold components on intersection or idle.
What's happening: Every batch of new items inserts 50 DOM nodes synchronously, causing a long task and dropping frames.
Fix: Pre-fetch the next batch before the user reaches it, insert nodes in chunks of 5–10 with scheduler.yield() between chunks, or virtualize the list.
What's happening: The form's submit handler runs synchronous validation over many fields, blocking the main thread for 250+ ms before the click visibly registers.
Fix: Show an immediate disabled/loading state synchronously, then move the validation pass off the main thread or into chunked work with scheduler.yield().
Long tasks are the underlying primitive. TBT, INP, and TTI are aggregations of long-task behavior across different windows.
| Metric | What It Measures | Good Threshold | Connection to Long Tasks |
|---|---|---|---|
| Total Blocking Time (TBT) | Sum of blocking time during load (lab metric) | ≤ 200 ms | TBT = Σ(task − 50 ms) for every long task between FCP and TTI. |
| Interaction to Next Paint (INP) | Worst real-user input response (Core Web Vital) | ≤ 200 ms | INP is dominated by long tasks during input handling and rendering. |
| Time to Interactive (TTI) | When the page can reliably respond to input | ≤ 3.8 s | TTI fires after a 5-second window with no long tasks. |
| JavaScript Bootup Time | CPU time spent parsing, compiling, executing JS | ≤ 2 s | Bootup is the source of most load-time long tasks. |
| First Input Delay (FID) | Delay before the first input is processed (deprecated 2024) | ≤ 100 ms | Replaced by INP. Both are dominated by long tasks. |
It comes from Google's RAIL model. Humans perceive a response as "instant" only within ~100 ms; the browser needs about 50 ms of that for event dispatch and paint, so any single chunk of pure work over 50 ms eats into the budget the user can feel.
Indirectly, yes. Long tasks themselves aren't a ranking factor, but they directly determine INP, which became a Core Web Vital in March 2024 and is a confirmed input to Google's Page Experience ranking signal. Eliminating long tasks during interactions is the most reliable way to pass INP.
Long tasks are the underlying events; TBT is an aggregation. Specifically, TBT is the sum of (task_duration − 50 ms) for every long task between FCP and TTI. A page can have one 300 ms long task (TBT contribution: 250 ms) or six 100 ms long tasks (TBT: 300 ms) — different shapes, similar pain.
Only the ones that don't touch the DOM. Workers are perfect for parsing large JSON, search/filter over big datasets, image processing, and cryptography. Anything that mutates the DOM (rendering, layout, hydration) has to stay on the main thread — for those, the fix is yielding (scheduler.yield()), virtualization, or shipping less work.
Use a PerformanceObserver with type 'longtask'to capture them on real devices, send the data to your real-user monitoring, and alert when the 75th percentile of long-task duration crosses 100 ms on any high-traffic page. Greadme's crawler scan also surfaces this data per template.
Indirectly. AI answer engines preferentially cite pages that already rank well in traditional search. Long tasks degrade INP, which degrades Page Experience, which lowers rankings — and lower-ranking pages get cited less in Google AI Overviews, Perplexity, and ChatGPT browsing results.
setTimeout(fn, 0) still a valid yielding pattern?It works, but scheduler.yield() is preferred when available. setTimeout(0) goes to the back of the task queue, which means other tasks (including unrelated ones) can run before yours resumes. scheduler.yield() is designed to resume your work next, with a single browser turn in between for input and paint.
Long tasks are the atomic unit of a sluggish website. Every bad TBT score, every failed INP, every rage-click on a button that "didn't work" traces back to one. The good news: the fixes are concrete and well-understood — yield, offload, virtualize, debounce, and stop hydrating things that don't need it.
Run a Greadme deep scan to see exactly which long tasks are running on your pages, what scripts caused them, and the AI-generated fix or one-click GitHub PR to ship the change.