/ JS Checker Fixes / Long Tasks

How to Fix Long JavaScript Tasks

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.

1. Find the long tasks

Step 1
DevTools Performance recording
DevTools → Performance tab → record button → reload the page → stop recording. Long tasks appear as red-flagged rectangles in the Main track. Click each to see the function call stack responsible.
Step 2
Lighthouse audit
"Avoid long main-thread tasks" audit in Lighthouse lists each long task with duration. "Reduce JavaScript execution time" audit shows which scripts contribute most.
Step 3
Field INP in Search Console
Search Console Core Web Vitals report shows real-user 75th-percentile INP. Pages with INP over 200ms have responsiveness issues from long tasks in production.

2. Break up long tasks

Splitting a long task into smaller chunks with yields to the browser keeps the page responsive even during heavy work.

Basic pattern with setTimeout

// 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));
    }
  }
}

Modern: scheduler.yield()

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.

3. Use scheduler.postTask for prioritised work

// 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).

4. Move CPU-heavy work to Web Workers

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.

When NOT to use a Worker

5. Defer non-critical initialisation

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.

6. Optimise hot paths

Some long tasks are unavoidable computation. Optimise the algorithm or data structures.

Memoize repeated computations

// 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;
}

Avoid layout thrashing

// 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
});

7. Measure improvement

Step 1
Lighthouse TBT
TBT under 200ms is Good per Core Web Vitals. Re-run Lighthouse after fixes; TBT should drop noticeably.
Step 2
Real-user INP via web-vitals library
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.
💡 The 80/20 of long-task fixes: find the top 3 tasks contributing most to TBT, fix them, ignore the rest. Heavy initialisation in third-party scripts, framework hydration, and synchronous data parsing on load account for most long-task time in typical sites.

⚙️ Re-run the JS Checker

Verify long-task findings are cleared and TBT/INP have improved.

Run JS Checker →
Related Guides: JS Checker Fixes  ·  Fix Memory Leaks  ·  Core Web Vitals Fixes  ·  JS Checker Guide
💬 Got a problem?