JavaScript long tasks keep the browser's main thread busy for more than 50 milliseconds. During that work, the browser cannot respond to a click, update the interface, or run another queued task. To fix JavaScript long tasks, record the deployed page, identify the work inside each task, remove unnecessary work, and split or move the rest before testing the same route again.
This guide focuses on practical load-time diagnosis for developers. It complements the Core Web Vitals guide, which explains the broader relationship between lab and field performance signals.
JavaScript long tasks checklist
Use this short pass on the public URL you plan to ship:
- Record a mobile page load in the Chrome DevTools Performance panel.
- Find tasks longer than 50 milliseconds and inspect their call stacks.
- Separate first-party application work from framework and third-party work.
- Remove code that the initial route does not need.
- Split necessary work so the browser can handle input and rendering between chunks.
- Move suitable CPU-heavy work to a Web Worker.
- Repeat the same load conditions and keep a bounded regression check in CI.
The 50-millisecond boundary describes an individual long task, not a complete performance target. A page can contain many tasks just below that boundary, or become slow only after a user interaction. Use long-task evidence to locate main thread blocking, then confirm the effect with a trace and real-user responsiveness data when available.
What JavaScript long tasks are
The browser uses the main thread for work including JavaScript execution, style calculation, layout, and rendering. It processes one task at a time, and it runs the current task to completion before starting another. web.dev's long-task guide defines a task longer than 50 milliseconds as a long task. The part after the first 50 milliseconds is its blocking period.
For example, a 180-millisecond task contributes 130 milliseconds of blocking time:
task duration: 180 ms
free allowance: -50 ms
blocking period: 130 ms
The browser does not know that five functions inside one event handler represent separate conceptual steps. If they run synchronously in the same callback, they form one task. A click that arrives during that task waits until the callback, style work, and any synchronous follow-up work finish.
Common sources include:
- parsing, compiling, and evaluating a large application bundle;
- rendering a large component tree during startup;
- synchronous loops, data transforms, or JSON processing;
- repeated layout reads and writes that force rendering work;
- tag managers, analytics, chat widgets, and other third-party scripts;
- event handlers that combine user-visible updates with background work.
A long task is not automatically proof of bad JavaScript. Device speed, route state, extensions, experiments, and third-party responses change the result. It is evidence to investigate under named test conditions.
Find JavaScript long tasks in Chrome DevTools
Open the deployed page in Chrome, select DevTools → Performance, and record a reload. Use mobile CPU and network settings that match your release baseline. In the Main track, expand the long task and inspect the call tree or bottom-up view to find the functions responsible for most of its duration.
Chrome marks long tasks in a performance recording and lets you inspect their duration and stack. The Chrome long-task debugging guide recommends grouping the bottom-up view by activity to find expensive script evaluation, function calls, or DOM work.
Work from the first owned cause rather than the loudest symptom:
- Select the longest relevant task.
- Check whether it belongs to the application, a dependency, or a third party.
- Expand the stack until you reach code your team can change.
- Record what triggered it: initial load, hydration, timer, network callback, or input.
- Make one targeted change and compare a new trace under the same conditions.
Do not treat transfer size and execution time as interchangeable. A compressed script can be quick to download but expensive to parse and run. Conversely, a large cached resource may add little work to the main thread. Use the Network and Main tracks together.
Measure long tasks with PerformanceObserver
The Long Tasks API exposes PerformanceLongTaskTiming entries in supporting browsers. MDN's `PerformanceLongTaskTiming` reference documents each entry's start time, duration, and limited attribution data. It also marks the API as limited and experimental, so use it as focused browser evidence rather than a universal monitoring contract.
Run this in the page before the work you want to observe:
const longTasks = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
longTasks.push({
start: Math.round(entry.startTime),
duration: Math.round(entry.duration),
blocking: Math.max(0, Math.round(entry.duration - 50)),
container: entry.name,
});
}
});
observer.observe({ type: "longtask", buffered: true });
The container attribution does not identify the exact function that caused the task. Save a DevTools trace when you need the call stack. Also record the URL, viewport, CPU settings, cache state, browser version, and build revision so another developer can reproduce the result.
For longer-lived field investigation, consider the newer Long Animation Frames API. MDN's comparison of long animation frames and long tasks explains that long-animation-frame entries can expose contributing scripts, while the older Long Tasks API can miss rendering work and tasks that individually stay below 50 milliseconds.
Keep long tasks, Total Blocking Time, and INP separate
Long tasks are low-level performance entries. Total Blocking Time and Interaction to Next Paint answer different questions:
- A long task is one main-thread task that exceeds 50 milliseconds.
- Total Blocking Time (TBT) sums the blocking portions of long tasks in a defined lab measurement window after First Contentful Paint.
- Interaction to Next Paint (INP) is a field metric based on the latency of real page interactions.
The web.dev TBT definition explains that Lighthouse traditionally measures TBT from First Contentful Paint until Time to Interactive. It also recommends field INP for actual responsiveness: TBT can point to a likely problem in the lab, but it is not a substitute for how real interactions perform.
Name a custom sum accurately. If a test adds duration - 50 for every observed long task but does not use Lighthouse's measurement window and environment, call it a blocking-time proxy, not Lighthouse TBT. That distinction keeps regression data useful without pretending two different tests are interchangeable.
Fix main thread blocking at the source
Start by deleting or delaying work the current route does not need. Audit large dependencies, duplicate polyfills, startup analytics, and components rendered below the first view. Load route-specific code with dynamic import() and verify that the split removes execution from the critical path instead of merely creating more requests.
When necessary work is still too large, yield between batches. Moving helper functions into separate files does not split a task; control must return to the browser. The following pattern processes a queue while giving the browser regular opportunities to handle higher-priority work:
async function processJobs(jobs) {
let lastYield = performance.now();
for (const job of jobs) {
job();
if (performance.now() - lastYield >= 40) {
if (globalThis.scheduler?.yield) {
await scheduler.yield();
} else {
await new Promise((resolve) => setTimeout(resolve, 0));
}
lastYield = performance.now();
}
}
}
`scheduler.yield()` on MDN schedules a prioritized continuation, but it is not supported everywhere. Feature-detect it and keep a tested fallback. Choose the batch deadline from measurements; the example's 40 milliseconds is a starting point, not a universal budget.
Move CPU-heavy work that does not need the DOM to a Web Worker. Parsing a large local dataset, generating search indexes, image processing, and complex calculations can be good candidates. The web.dev Web Worker guide explains the message-based boundary and the APIs that are unavailable inside a worker. Account for serialization and coordination cost before moving small tasks.
After each change, verify the user-facing path. Faster startup that breaks ordering, drops analytics, or leaves a stale interface is not a successful optimization. The JavaScript console errors guide provides a separate check for runtime exceptions and failed requests introduced during refactoring.
Add a JavaScript long tasks regression check
A deployment check can capture obvious changes in a controlled Chromium load. Install the observer before navigation, then read a bounded summary after the route settles:
await page.addInitScript(() => {
window.__longTasks = [];
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
window.__longTasks.push(entry.duration);
}
}).observe({ type: "longtask", buffered: true });
});
const targetUrl = process.env.PREVIEW_URL;
if (!targetUrl) throw new Error("PREVIEW_URL is required");
await page.goto(targetUrl, { waitUntil: "load" });
await page.waitForTimeout(1000);
const summary = await page.evaluate(() => ({
count: window.__longTasks.length,
blockingTime: window.__longTasks.reduce(
(total, duration) => total + Math.max(0, duration - 50),
0,
),
}));
if (summary.blockingTime > 200) {
throw new Error(`Blocking-time proxy was ${summary.blockingTime}ms`);
}
Keep the browser, viewport, cache, CPU settings, wait window, and route stable. Run several samples before turning a noisy measurement into a hard merge gate, and save a trace when the result fails so the number leads to a fix. The website regression testing checklist covers reviewable baselines, while the GitHub Actions website audit guide explains how to attach deployed-page evidence to a pull request.
Check JavaScript long tasks with Slop Audit
Run a Slop Audit on the public preview or production URL after deployment. When Browser Run and the Long Tasks API are available, Slop Audit examines a bounded 375 × 812 mobile load, counts the exposed long-task entries, and sums the portion of each observed task beyond 50 milliseconds. It reports that result as a lab blocking-time proxy: 200 milliseconds or less passes, above 200 through 600 milliseconds warns, and above 600 milliseconds fails.
This evidence is not Lighthouse TBT, field INP, or a complete performance profile. It does not name the responsible function, cover every later interaction, simulate the range of user devices, or guarantee that the API exposed every source of main-thread blocking. Use the result to catch a clear launch regression, reproduce it in DevTools, fix the owned work, and rerun the same URL.
JavaScript long tasks release checklist
- Test the deployed route at a named mobile viewport and CPU setting.
- Inspect the call stack for each material long task instead of optimizing by guesswork.
- Remove startup code and third-party work the route does not need.
- Split necessary synchronous work by yielding control to the browser.
- Move suitable CPU-heavy work off the main thread.
- Confirm that lazy loading and code splitting do not create broken requests or delayed critical UI.
- Label custom blocking-time sums as proxies with a defined measurement window.
- Keep lab evidence separate from field INP data.
- Save a trace or function-level evidence when a regression check fails.
- Repeat the same conditions after every fix.
Once the main thread is responsive, use the Largest Contentful Paint guide to check whether JavaScript still delays the primary content, then complete the mobile website testing guide around the route's actual user task.