How to Minimize Main-Thread Work? Complete Guide (2026)

Saar Twito7 min read
Saar Twito
Saar TwitoFounder & SEO Engineer

Hi, I'm Saar - a software engineer, SEO specialist, and lecturer who loves building tools and teaching tech.

View author profile →

What Is Main-Thread Work?

The browser's main thread parses HTML and CSS, runs JavaScript, handles user input, and paints frames. When a single task on this thread runs longer than 50ms it is classified as a "long task" and blocks input, which directly hurts Interaction to Next Paint (INP). The "Minimize main-thread work" audit sums the total time spent on script evaluation, style and layout, painting, and parsing during page load.

Key Facts (TL;DR)

  • A task over 50ms on the main thread is a "long task" (W3C Long Tasks API).
  • INP replaced FID as a Core Web Vital on March 12, 2024 — good INP is under 200ms.
  • Script evaluation is the largest contributor to main-thread time on most sites (HTTP Archive, 2025).
  • Web Workers run in a separate thread and never block input or rendering.
  • content-visibility: auto can skip rendering work for offscreen sections, often cutting style and layout time by 30 to 50 percent.
  • Automated audits flag this issue when total main-thread work exceeds 4 seconds on a simulated mid-tier mobile.

What Causes Main-Thread Blocking?

Each cause maps to a specific Core Web Vital. The table below pairs the cause with the metric it hurts and the canonical fix.

CauseMetric AffectedFix Pattern
Large JavaScript bundlesTBT, INP, LCPCode-split with dynamic import(), tree-shake, defer non-critical JS
Long synchronous tasks (>50ms)INP, TBTYield with scheduler.yield() or setTimeout(fn, 0), chunk work
Layout thrashing (read/write/read DOM)INP, CLSBatch reads, then batch writes; use requestAnimationFrame
Unoptimized React rendersINPReact.memo, useMemo, list virtualization
Heavy CSS recalc on long pagesLCP, INPcontent-visibility: auto for offscreen sections
CPU-bound computationINP, TBTMove to a Web Worker
Frequent scroll/resize handlersINPDebounce or throttle; use requestIdleCallback

How to Minimize Main-Thread Work: Step by Step

  1. Record a Performance trace in Chrome DevTools and sort the Bottom-Up tab by Self Time.
  2. Identify long tasks — anything red-striped over 50ms is a candidate.
  3. Code-split the largest bundle using dynamic import() for routes and modals.
  4. Yield inside long loops with await scheduler.yield() (Chrome 129+) or await new Promise(r => setTimeout(r, 0)).
  5. Move pure computation off-thread by wrapping it in a Web Worker.
  6. Add content-visibility: auto to long lists, comments, and footers.
  7. Re-run an automated audit and confirm it passes (under 2 seconds of main-thread work is the target).

Yield inside a long loop

// Bad: one 800ms task that blocks every input
function processAll(items) {
  for (const item of items) heavyWork(item);
}

// Good: yield every 5ms so the browser can handle input
async function processAll(items) {
  let deadline = performance.now() + 5;
  for (const item of items) {
    heavyWork(item);
    if (performance.now() > deadline) {
      await scheduler.yield();           // Chrome 129+
      deadline = performance.now() + 5;
    }
  }
}

Offload computation to a Web Worker

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

// parser.worker.js
self.onmessage = (e) => {
  const result = parseAndIndex(e.data); // CPU-bound, never touches the DOM
  self.postMessage(result);
};

Skip offscreen rendering with content-visibility

.comment, .product-card {
  content-visibility: auto;
  contain-intrinsic-size: 0 240px; /* reserve space to avoid CLS */
}

Common Mistakes

  • Using setTimeout(fn, 0) as the only chunking strategy — it still posts macrotasks; scheduler.yield() resumes faster.
  • Creating a Web Worker per call instead of reusing a worker pool.
  • Reading offsetHeight inside a write loop, forcing forced synchronous layout on every iteration.
  • Lazy-loading every route, including the LCP route, which delays the largest paint.
  • Adding content-visibility: auto without contain-intrinsic-size, which causes scrollbar jumping.

How to Test Main-Thread Performance

  1. Run an automated audit in Chrome DevTools with mobile emulation and check the "Minimize main-thread work" result.
  2. Open the Performance panel, record page load, and look at the Main track for long tasks.
  3. Use the web-vitals JS library in production to capture real-user INP.
  4. Check a Greadme deep scan for both lab and field (CrUX) data.
  5. Compare before and after with performance.measure() markers in code.

For deeper context, see our guides on Total Blocking Time and long tasks.

FAQ

What is the "Minimize main-thread work" audit?

It is a diagnostic that sums every category of main-thread work during page load — script evaluation, style and layout, parsing, painting, and garbage collection — and flags pages where the total exceeds roughly 4 seconds on a simulated mid-tier mobile device.

How is main-thread work different from Total Blocking Time?

Total Blocking Time only counts the portion of long tasks above the 50ms threshold between FCP and TTI. Main-thread work is the total time, including short tasks.

Does INP measure the main thread?

Yes. INP measures the longest input latency the user experienced, which is dominated by main-thread blocking — long tasks, expensive event handlers, and rendering work after the handler runs.

Will moving everything to a Web Worker fix INP?

No. Workers cannot touch the DOM, so the main thread still does layout, paint, and event handling. Workers help with pure computation but you also need to optimize handlers and rendering.

Should I use requestIdleCallback for everything non-critical?

Use it for genuinely deferrable work such as analytics flushing or preloading. It is not supported in Safari, so use the safari-aware scheduler.postTask with priority: 'background' when available.

Does code-splitting always reduce main-thread time?

Almost always at initial load, but it can hurt INP if a user interaction triggers a large chunk download. Preload critical chunks with <link rel="modulepreload">.

How does content-visibility affect main-thread work?

It tells the browser to skip rendering work — style, layout, and paint — for elements that are not in the viewport. On long pages this can cut main-thread render time by 30 to 50 percent.

Does this affect AI search engines like ChatGPT and Perplexity?

Indirectly, yes. AI search engines preferentially cite pages that already rank well in traditional search, and INP is a Core Web Vital ranking signal — so heavy main-thread work that delays input response can lower rankings, which lowers citation odds. Long main-thread tasks also delay content rendering, sometimes past the timeouts AI crawlers use when fetching a page.

Conclusion

Minimizing main-thread work means cutting long tasks below 50ms, splitting bundles, moving computation to Web Workers, and skipping offscreen rendering with content-visibility. Profile in Chrome DevTools, fix the longest tasks first, and validate with field INP data — the audit fails when total main-thread work exceeds about 4 seconds on a mid-tier mobile, so aim for under 2 seconds. Run a Greadme deep scan to identify pages with heavy main-thread work across your site.