Minimize main thread work and get TBT under 200ms

TL;DR

“Minimize main-thread work” is a diagnostic—the metrics that matter are TBT (under 200ms on mobile) and INP. Work in order of leverage: audit third-party scripts, split long tasks with scheduler.yield(), defer or delay non-critical scripts, trim your JavaScript payload, then check CSS. Developers can do it by hand in DevTools; WordPress owners can automate most of it with NitroPack (free plan to test). If TTFB is slow, fix that first.

You ran your site through Lighthouse or PageSpeed Insights, and there it is—”Minimize main-thread work,” flagged in orange, with your Total Blocking Time sitting higher than you’d like. Chances are you’ve already tried a few fixes and watched the number barely budge.

The main thread is where the browser does nearly all of its work: parsing HTML and CSS, running JavaScript, calculating layout, painting pixels, and responding to clicks. When one task holds it for longer than 50ms, user input has to wait.

This guide covers the fixes that actually pull TBT under 200ms on mobile—Google’s “good” threshold—and why each one works.

What the main thread work audit is telling you

The audit flags any page whose main thread stays busy for more than four seconds during load. That’s the trigger. But the number itself isn’t what you’re chasing—”Minimize main-thread work” is a diagnostic, not a scored metric, so it doesn’t directly change your Performance score. What it does change are the two metrics that count: Total Blocking Time and Interaction to Next Paint.

🤓 Here’s what you need to remember!

TBT is a lab proxy for INP, which is the field metric Google actually ranks on. You can’t reliably measure INP in a single lab run, so TBT stands in for it while you work. Get TBT down, and INP tends to follow.

The report also splits main-thread time into seven categories, and each one points to a different fix later in this guide:

  • Script evaluation—running your JavaScript.
  • Style & layout—working out what goes where.
  • Rendering—painting and compositing pixels.
  • Parse HTML & CSS—reading the markup.
  • Script parsing & compilation—preparing JS to run.
  • Garbage collection—reclaiming memory.
  • Other—lower-level work that doesn’t fit the rest.

Match the category eating your budget to the section that fixes it.

Finding the scripts behind each Lighthouse category

The frustrating part of the audit is that it gives you a category and a number—”Script evaluation: 4.2s”—but never names the script behind it. Chrome DevTools can help with that.

  1. Open the Performance panel and record a page load. 
Opening the Performance panel in Chrome DevTools and choosing “Record and reload”
  1. Switch to Bottom-Up view and group by URL or domain to separate your own code from third-party scripts—analytics, ad pixels, tag managers, chat widgets. 
Choosing the Bottom-up view and grouping results by URL 
  1. Sort by Self Time to find the most expensive calls, then expand the heaviest tasks down to specific files and functions. 
Sorting results by Self time and expanding an activity

Long tasks over 50ms carry a red triangle, so they stand out. If you’re chasing INP rather than load-time TBT, record yourself clicking and scrolling too, not just the initial load.

Two features do most of the work. The “3rd parties” insight breaks execution cost down by vendor, and the Coverage tab (Cmd/Ctrl+Shift+P → “Show Coverage”) shows how much JavaScript in each file goes unused. You can use that information for the code-splitting section later.

Once you know what’s expensive, the category points you toward the fix:

  • Script evaluation: Defer or remove JS, split long tasks, or move work off the main thread.
  • Style & layout, Rendering, Parse HTML & CSS: Simplify the DOM, cut paint cost, and defer stylesheets.
  • Script parsing & compilation: Ship less JavaScript up front.
  • Garbage collection: Ease memory pressure and fix leaks.
  • Other: Inspect the trace directly; it’s a catch-all, not one problem.

On most WordPress sites, the biggest numbers sit in script evaluation and third-party code—so that’s where we start.

How to minimize main thread work

Each fix below targets one or more of the categories from the diagnostic above, ordered by how much of a difference they can make on a WordPress site. We start with third-party scripts—usually the single biggest source of main-thread time—then work through long tasks, loading strategy, payload size, off-thread processing, and CSS.

Auditing and managing third-party scripts

On a marketing-heavy WordPress site, third-party scripts are usually the single largest source of main-thread work. Analytics, ad pixels, chat widgets, review apps—each runs its own JavaScript, all competing for the same thread. Thankfully, you don’t have to get rid of them all immediately, but you will have to make a decision for each one.

Run every external script through five options:

  1. Remove it. If it’s duplicated, unused, or no longer earning its keep for marketing, delete it. The cheapest script is the one that isn’t there.
  2. Load it normally. Reserve this for scripts genuinely required before the page can render correctly—consent management, session security, a business-critical above-the-fold feature. This should be the rare exception, not the default.
  3. Defer it. For scripts needed on the page but not for the first paint or first interaction. Analytics is a common candidate. A/B testing tools need care: prefer server-side testing, or load only the minimum early code to avoid content flicker.
  4. Delay until interaction. For scripts that only matter once a visitor engages or consents—chat widgets, heatmaps, session recording, review widgets, popups, social embeds. They load on the first scroll, click, or touch.
  5. Replace it with a facade. Show a lightweight static placeholder first, then load the real embed when the user clicks. This works well for YouTube and Vimeo players, maps, booking widgets, and comment sections.

⚠️ Important for Google Tag Manager!

The container itself is light, but the tags inside it block the thread. Delaying the whole container breaks the data layer and your tracking with it. You can fix this by delaying the heavy scripts inside the container while leaving the container and data layer intact.

To find your worst offenders, pair the “Third parties” insight in Lighthouse with the DevTools workflow from the last section. In the Network panel, right-click a script and “Block request URL” to estimate its impact. Test across a few runs, since blocking one script can shift how others behave.

Doing this by hand never stays done—marketing keeps adding scripts, and exclusion lists drift out of date fast, especially across client sites. NitroPack automates it: Delayed Scripts (Plus and up) holds non-critical scripts until a user interacts, and Optimize GTM (Pro and Agency) delays the tags inside the container while keeping the data layer intact—no exclusion list to maintain.

Important: None of this affects first-party application JavaScript that must run on every page. That’s what the next few sections handle.

Breaking up long tasks with scheduler.yield

Any main-thread task that runs longer than 50ms is a long task. While it runs, the browser can’t stop to handle a click or a keypress—input just waits until the task finishes. For TBT, only the time beyond that first 50ms counts as blocking, so a 220ms task contributes 170ms of blocking time.

scheduler.yield() is the modern way to break those tasks up. It pauses an async function and schedules the rest as a prioritized continuation—which usually runs ahead of other similarly prioritized work in the queue, while still letting higher-priority browser work, like input, go first. In practice, you find a long task in DevTools, split its work into chunks, and yield between them so the browser can breathe.

The trick is knowing when to yield. Yielding after every item adds its own overhead, so only give the thread back once the current batch has run past 50ms. web.dev’s own cross-browser example comes in two parts. 

First, define a yieldToMain() helper once — it uses scheduler.yield() where the browser supports it and falls back to setTimeout where it doesn’t:

function yieldToMain () {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }

  // Fall back to yielding with setTimeout.
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

Then call that helper inside your job loop, yielding only after the batch passes the 50ms deadline:

async function runJobs(jobQueue, deadline=50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    // Run the job:
    job();

    // If it's been longer than the deadline, yield to the main thread:
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

Support is solid but not universal: Chrome 129+, Edge 129+, and Firefox 142+ ship scheduler.yield() natively, while Safari doesn’t yet—which is why the helper feature-detects before falling back. For behavior closer to native across browsers, drop in the scheduler-polyfill instead, though it can’t perfectly reproduce native priority. The setTimeout fallback keeps the page responsive; it just loses the prioritized continuation.

Re-run the DevTools recording to confirm it worked: the long tasks should split into shorter ones, and TBT should drop.

This one is pure code-level work—there’s no NitroPack setting for it. For how it fits with INP more broadly, our Core Web Vitals guide for 2026 has the wider context. Splitting tasks keeps necessary JavaScript from freezing the page—but the next lever is controlling when scripts run at all.

Defer, async, and delay until interaction

These three loading strategies get used interchangeably, and getting them wrong either breaks functionality or leaves the optimization on the table. The right one depends on what the script actually needs.

StrategyWhen it runsUse it forAvoid it for
deferAfter HTML parsing, in source orderScripts that need the parsed DOM or must run in a set order (with their dependencies also deferred, in order)Anything required for first paint
asyncAs soon as it downloads, ignoring orderFully independent scripts—isolated trackers, some analyticsScripts with dependencies, anything relying on jQuery, or code that assumes the DOM is ready
Delay until interactionOnly after a user action—click, scroll, tap, keypressChat widgets, video players, review widgets, anything post-engagementScripts that must render initial content or set up required global state

The most common break is jQuery. Loading a jQuery-dependent script with async means $(document).ready() can fire before jQuery exists, and the script errors out—defer preserves source order, so dependent scripts stay safe. Watch one trap with defer, though: a deferred script still runs on the main thread once parsing finishes, so a heavy one can still block. Pair it with the task-splitting from the last section.

Note

Delay-until-interaction is the most aggressive option, so keep two things off it: analytics (you’ll lose bounce and early-engagement data) and consent management (a compliance risk). Making these calls script by script is ongoing work as new scripts ship—NitroPack handles the defer and delay decisions automatically, with Safe Mode and manual exclusions as backstops.

Reducing JavaScript payload through code splitting

Every kilobyte of JavaScript you ship has to be parsed, compiled, and evaluated on the main thread—which is why bundle size drives the script parsing & compilation and script evaluation categories. The fix is to send less code and send it only where it’s needed.

⚠️ Heads up

Minification is table stakes, not your TBT fix. It shrinks files by stripping comments and whitespace, which helps parsing and download—but barely touches the cost of running the code. And it’s usually already handled: most hosts and performance tools, NitroPack included, minify automatically.

On WordPress, the real lever is conditional enqueuing. Themes and plugins routinely load their JavaScript on every page even when it only fires on one—a homepage slider has no business loading on a product page. Work through it in three passes:

  • Audit what’s loading sitewide versus what only fires on specific templates.
  • Enqueue conditionally with wp_enqueue_script() on the wp_enqueue_scripts hook, paired with conditional tags like is_front_page(), is_singular('product'), or is_page_template().
  • Check your plugins the same way—they’re often the worst offenders.

On a custom build, the toolkit changes. Modern block themes, WooCommerce, and React-based plugins often ship a bundler, so reach for tree shaking to drop unused exports and dynamic import() to split routes and heavy components so they load on demand.

One honest limit: Build-time code splitting is developer work. A performance tool can minify, compress, cache, defer, and delay your scripts—but it can’t safely redesign your app’s bundle structure after the fact. When code has to run but doesn’t need the main thread, though, you can move it off the thread entirely.

Moving heavy work off the main thread with Web Workers

Web Workers run JavaScript in a background thread, leaving the main thread free to respond to input. The mental model is the important part: a Worker doesn’t make the work faster—it moves it somewhere that doesn’t freeze the page while it runs.

That makes them a fit for heavy, self-contained jobs—crunching large datasets, filtering big arrays, parsing hefty JSON, image manipulation, building a search index. The problem is that a Worker can’t touch the DOM, so anything that updates the page has to pass its results back to the main thread.

Two libraries make the pattern practical. Comlink wraps the awkward postMessage API so calling into a Worker feels like a normal async function, and Partytown proxies third-party scripts through a Worker—though it’s still beta, and not every script runs cleanly without configuration. Workers handle the JavaScript you can’t remove—but not all main-thread time is JavaScript. If yours is already lean and the number won’t budge, CSS is the next place to look.

Cutting CSS and style recalculation costs

If your JavaScript is already lean and the main-thread number still won’t drop, CSS is the usual culprit. Three of the seven Lighthouse categories trace back to it—style & layout, rendering, and parse HTML & CSS—and none of the JavaScript fixes above touch them.

Four things drive CSS onto the main thread:

  • Bloated critical path. Inline the CSS for above-the-fold content and load the rest asynchronously, so the browser isn’t parsing your entire stylesheet before it paints.
  • Complex selectors. Deeply nested rules and universal selectors (*) slow style recalculation in large DOMs. Flatten where you can, and skip the universals.
  • Layout thrashing. Reading a layout value and then writing to the DOM in the same loop forces the browser to recalculate layout on every pass. Batch your reads, then your writes—fastdom enforces the pattern for you.
  • Main-thread animations. Animating width, top, or left runs layout on every frame. Stick to transform and opacity, which the compositor handles off the main thread.

Critical CSS is the fiddliest to maintain by hand because it needs regenerating every time a template changes. NitroPack generates it per page automatically, and Remove Unused CSS strips the rules a page never uses. 

How these fixes move TBT under 200ms and improve INP

Every fix in this guide maps to a metric. Auditing third-party scripts, splitting long tasks, and deferring or delaying non-critical code all cut the blocking time that piles up during load—that’s TBT. 

Do all of them together, and the main thread stays free while people actually use the page, which is what moves INP, the field metric Google ranks on. Both share the same “good” threshold: 200ms or under, INP measured on real interactions since it replaced First Input Delay as a Core Web Vital in March 2024.

One thing to remember: These numbers are judged on mid-range mobile hardware, not your dev laptop—so test the way Google does.

If your Time to First Byte is slow because of weak hosting or backend code, main-thread work isn’t the problem—no amount of script deferral fixes a slow server response. Diagnose TTFB separately first.

However, if that’s not the case, then you have two options: Apply every technique above by hand and keep maintaining it as the site changes, or automate the categories that are hardest to do manually. 

NitroPack’s JavaScript Optimization covers the ones in this guide:

  • Delayed Scripts (Plus and up) holds non-critical scripts until a user interacts.
  • Optimize GTM (Pro and Agency) delays the heavy tags inside the container while keeping the data layer intact.
  • Per-page Critical CSS is generated automatically for every page, with no manual regeneration when a template changes.
  • Remove Unused CSS (Plus and up) strips the rules a page never uses
  • Defer and async automation with Safe Mode as a fallback if a script misbehaves.

There’s also Optimize Interactive Elements (Plus, Pro, and Agency), which gives interactions instant visual feedback—documented gains of 36% INP on desktop and 32% on mobile, with no code refactoring.

⭐ Client spotlight 

When Norwegian agency Mikalsen Utvikling switched to NitroPack, their mobile PageSpeed score went from 24 to 94—reduced main-thread blocking working alongside image and render-blocking fixes.

You don’t have to commit to find out. NitroPack’s free plan needs no credit card and takes about three minutes to set up.

FAQs

Why does PageSpeed Insights show a different score than my local Lighthouse run?

PSI runs on Google’s infrastructure and applies network latency and a CPU-throttling multiplier to mimic a mid-range mobile device. The faster your own machine, the bigger that multiplier, so a powerful dev laptop can actually score lower in PSI than in a local run. That gap is expected, not a bug in either tool. Treat the lab score as your diagnostic and field INP as the metric that ranks.

What is the “Other” category, and why is it sometimes so large?

“Other” is one of the seven Lighthouse main-thread task groups—the catch-all for lower-level work that doesn’t fit the named categories. A large “Other” bucket is sometimes inflated by lab conditions, since visually complex pages tested without GPU acceleration lean on software rendering. But don’t assume it’s always an artifact. 

Compare PSI against a local Lighthouse run and the DevTools trace, and if the page is genuinely complex, cut paint, compositing, and animation cost.

I already have a caching plugin. Is that enough?

Caching tools handle minification and basic deferral well, but they can’t reduce the raw execution cost of third-party scripts, and they don’t touch garbage collection—where a lot of stubborn main-thread time lives. NitroPack’s cloud architecture can perform fixes that server-based tools can’t, including Delayed Scripts, Optimize GTM, and per-page Critical CSS.

How do I reduce main thread work in a Next.js or React application?

The principles are the same; the implementation differs. Use next/dynamic for component-level splitting, move logic into Server Components, and load third-party scripts with next/script and strategy="lazyOnload". Most React TBT comes from large initial bundles and hydration cost, so profile with the React DevTools Profiler to find the expensive components. 

Which tools measure TBT continuously?

For one-off lab checks, you’re already covered—Lighthouse, PageSpeed Insights, and the DevTools Performance panel. What they don’t do is watch for drift: TBT and INP creep back up as the site and its third-party scripts change. For that, DebugBear runs ongoing monitoring, and the CrUX dashboard tracks field INP over time. Lab tools tell you today’s state; monitoring tells you when a new script has set you back.

Niko Kaleev

By Niko Kaleev

User Experience Content Expert

Niko has 5+ years of experience turning those “it’s too technical for me” topics into “I can’t believe I get it” content pieces. He specializes in dissecting nuanced topics like Core Web Vitals, web performance metrics, and site speed optimization techniques. When he’s taking a breather from researching his next content piece, you’ll find him deep into the latest performance news.