Long tasks are JavaScript that monopolises the main thread for over 50ms. During a long task, the browser can't respond to clicks, taps, or scrolls. Total Blocking Time (TBT) measures pre-interactive blocking; Interaction to Next Paint (INP) measures interactive responsiveness across the whole session. Both Core Web Vitals reflect long tasks directly. The fix combines breaking up CPU-bound work, yielding to the browser, and moving heavy computation to Web Workers.
Splitting a long task into smaller chunks with yields to the browser keeps the page responsive even during heavy work.
// Before: 200ms task processing 10,000 items
function processItems(items) {
for (const item of items) {
expensiveOperation(item);
}
}
// After: yields every 50ms
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
expensiveOperation(items[i]);
if (i % 100 === 0) {
// Yield to browser
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
expensiveOperation(items[i]);
if (i % 100 === 0) {
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
}
scheduler.yield() resumes faster than setTimeout(0) because it prioritises returning to your task. Supported in Chrome 129+ and Edge 129+; fall back to setTimeout for others.
// Available in Chrome 94+, Edge 94+
if ('scheduler' in window && 'postTask' in scheduler) {
scheduler.postTask(() => {
// Background work, low priority
refreshCache();
}, { priority: 'background' });
scheduler.postTask(() => {
// User-blocking work, runs first
updateUI();
}, { priority: 'user-blocking' });
}
Three priority levels: user-blocking (interactive responses), user-visible (default, things user can see), background (deferrable, like prefetching).
For genuinely heavy work (parsing large data, image processing, crypto, search indexing), Web Workers run on a separate thread.
// main.js
const worker = new Worker(new URL('./parser-worker.js', import.meta.url));
worker.postMessage({ csv: largeCsvString });
worker.onmessage = (event) => {
console.log('Parsed:', event.data);
};
// parser-worker.js (runs in separate thread)
import Papa from 'papaparse';
self.onmessage = (event) => {
const result = Papa.parse(event.data.csv, { header: true });
self.postMessage(result.data);
};
Main thread stays responsive while the worker parses. postMessage serialises data — keep payloads small or use SharedArrayBuffer for large data.
Not all JS needs to run on initial load. Use requestIdleCallback for work that can wait.
// Defer analytics and prefetch until idle
requestIdleCallback(() => {
initAnalytics();
prefetchLikelyPages();
warmCaches();
}, { timeout: 2000 }); // safety: run within 2s regardless
Browser calls back during idle periods between user interactions. If the page never idles within 2s, the timeout forces execution.
Some long tasks are unavoidable computation. Optimise the algorithm or data structures.
// Slow: recalculates on every render
function expensiveCalc(data) {
return data.reduce((acc, item) => acc + heavyMath(item), 0);
}
// Fast: cache by input
const cache = new Map();
function expensiveCalc(data) {
const key = JSON.stringify(data);
if (cache.has(key)) return cache.get(key);
const result = data.reduce((acc, item) => acc + heavyMath(item), 0);
cache.set(key, result);
return result;
}
// Bad: reading and writing to DOM in a loop forces multiple layouts
items.forEach(item => {
const width = item.offsetWidth; // forces layout
item.style.width = width * 2 + 'px'; // invalidates layout
});
// Good: batch reads, then batch writes
const widths = items.map(item => item.offsetWidth); // batch reads
items.forEach((item, i) => {
item.style.width = widths[i] * 2 + 'px'; // batch writes
});
import { onINP } from 'web-vitals';
onINP(metric => {
console.log('INP:', metric.value);
// Send to analytics
navigator.sendBeacon('/analytics', JSON.stringify(metric));
});
Field data shows the real picture. Pages where INP drops from 400ms to 150ms after long-task fixes have noticeably better responsiveness for users.
Verify long-task findings are cleared and TBT/INP have improved.
Run JS Checker →