Quick answer: This audit fails when the browser main thread stays busy too long during load, usually past about 4 seconds on the mobile profile. Read the category table first: Script Evaluation is the largest row on most sites. Fix it by shipping less JavaScript, deferring third-party scripts, and splitting long tasks so input can be handled between them.
This Lighthouse audit fails when the browser's main thread spends too long busy during page load. The main thread is a single queue: it parses HTML, runs every line of JavaScript, recalculates styles, lays out the page, paints it, and handles every click and tap. While it is busy with one of those, it cannot do any of the others, and it cannot respond to the user. "Minimize main-thread work" is the total of that busy time, broken into categories so you can see which kind of work is eating it.
TL;DR
- What: Total main-thread busy time during load, split into Script Evaluation, Style & Layout, Script Parsing & Compilation, Garbage Collection, Rendering, Parse HTML & CSS, and Other.
- Why it matters: It is the raw material behind Total Blocking Time and INP. A busy main thread means taps, scrolls and clicks queue up behind work the user did not ask for.
- Fix: Attack the largest category first. In practice that is almost always Script Evaluation: ship less JavaScript, defer what is not needed for first render, and break up long tasks.
What does the "Minimize main-thread work" audit measure?
Lighthouse records a trace of the page load, attributes every main-thread task to a category, and sums the durations. The report shows a table like this one, taken from a real run:
| Category | Time spent | What it is |
|---|---|---|
| Script Evaluation | 3,444 ms | Executing JavaScript: framework boot, hydration, third-party tags |
| Style & Layout | 627 ms | Recalculating styles and computing element geometry |
| Other | 520 ms | Main-thread work the trace could not attribute elsewhere |
| Script Parsing & Compilation | 453 ms | Turning downloaded JavaScript text into executable code |
| Garbage Collection | 253 ms | Reclaiming memory your scripts allocated |
| Rendering | 94 ms | Paint, composite and layerize |
| Parse HTML & CSS | 62 ms | Building the DOM and CSSOM from bytes |
That total is 5,456 ms of main-thread work, and the audit scores 0.
Three properties of the measurement matter when you read your own report:
- It is a sum, not a wall-clock duration. 5.4 seconds of main-thread work does not mean the load took 5.4 seconds; some of it overlaps with network time. It means the thread was unavailable for that long in aggregate.
- It is measured under throttling. PageSpeed Insights simulates a mid-tier mobile device and a slow 4G connection by default. The number on your laptop will be several times smaller and is not the number being scored.
- The category split is the useful part. The total tells you there is a problem; the split tells you which of six or seven very different fixes applies. Never optimise before reading it.
What is a good main-thread work number?
On the simulated mobile profile Lighthouse scores this audit on a curve rather than a hard cutoff. Roughly:
- Under about 2 seconds: green.
- Around 4 seconds: the middle of the distribution, scoring orange.
- Past roughly 4.5 seconds: red, which is where most content sites and nearly every storefront land.
Do not chase the colour. The audit carries no direct weight in the Performance score; it is diagnostic. What it feeds does carry weight: main-thread work is the substance that Total Blocking Time measures the user-visible cost of, and TBT is 30% of the mobile Performance score. Cutting 1,500 ms of Script Evaluation usually shows up as a TBT improvement and a score improvement even though this audit is not scored itself.
How is this different from Total Blocking Time and "Reduce JavaScript execution time"?
These three audits look at the same trace and answer different questions. Mixing them up leads to optimising the wrong thing.
| Audit | Question it answers |
|---|---|
| Minimize main-thread work | How much total main-thread work is there, and what kind? |
| Total Blocking Time | How much of that work landed in tasks over 50 ms, which is the part users feel? |
Reduce JavaScript execution time (bootup-time) | Which individual scripts and origins are responsible, file by file? |
The practical workflow reads them in that order. This audit tells you the problem is Script Evaluation rather than Style & Layout. bootup-time names the files. TBT tells you whether fixing them will move the score.
The distinction between total work and blocking time is the one that catches people. Four hundred 10 ms tasks and one 4,000 ms task are identical here and completely different for the user. The first is responsive; the second freezes the page. If this audit is high but TBT is low, your work is already well chunked and the payoff from further splitting is limited.
How do I fix "Minimize main-thread work"?
Read the category table, find the largest row, and apply the matching playbook.
Script Evaluation is the largest row
This is the usual case, and it means you are running too much JavaScript, too early.
- Ship less. Audit the bundle with
source-map-explorerorwebpack-bundle-analyzer. Moment.js with every locale, a full icon set, a charting library on a page with no chart: these are routine finds worth hundreds of milliseconds each. See Reduce unused JavaScript. - Defer what is not needed for first render. Add
deferto classic scripts so they execute after parsing rather than blocking it. Route-split with dynamicimport()so a visitor to the homepage does not evaluate the checkout bundle. - Delay third parties until the page is interactive. Analytics, chat widgets, A/B testing, heatmaps and review widgets rarely need to run during load. Loading them inside
requestIdleCallbackafter theloadevent costs nothing measurable and often removes a full second of evaluation. - Break up long tasks. Any task over 50 ms blocks input for its whole duration. Split loops with
await scheduler.yield()where it is available, orawait new Promise(r => setTimeout(r, 0))as the portable fallback, so the browser can service input between chunks. - Move real computation off the thread. Parsing a large JSON payload, filtering ten thousand products, image or crypto work: these belong in a Web Worker. The main thread posts a message and stays free.
Style & Layout is the largest row
The browser is recalculating geometry far more often than it needs to.
- Find forced reflows. Reading a layout property (
offsetHeight,getBoundingClientRect(),scrollTop) right after writing a style forces the browser to lay out synchronously. In a loop, that is a reflow per iteration. Batch all reads, then all writes. - Simplify the selectors and the tree. Deeply nested descendant selectors and a DOM of many thousands of nodes both make every recalculation more expensive. A large product grid rendered entirely up front is a common cause; virtualise it.
- Animate only
transformandopacity. Animatingwidth,topormargintriggers layout on every frame.transformandopacityare handled by the compositor and skip layout entirely. - Use
content-visibility: autoon long below-the-fold sections so the browser skips laying them out until they approach the viewport.
Script Parsing & Compilation is unusually large
You are shipping a large volume of JavaScript bytes, regardless of whether they run.
- Reduce the total bytes: code splitting and tree shaking cut parse cost proportionally.
- Avoid very large inline
<script>blocks, which cannot be streamed or cached. - Ship modern syntax. Legacy transpilation and polyfills can inflate a bundle by a third for browsers you no longer support.
Garbage Collection is unusually large
Something is allocating aggressively in a hot path: object creation inside animation frames or scroll handlers, large arrays rebuilt on every render, closures retained by listeners that are never removed. Profile with the DevTools Memory panel and reuse objects in the loops that show up.
"Other" is the largest row
This is usually third-party code the trace could not attribute, or extension noise. Re-run in an incognito window with extensions disabled to rule out the latter, then check the third-party section of the report. If a single origin dominates, that origin is the answer.
How do I fix this in React, WordPress, or Shopify?
React and Next.js
Hydration is the single biggest source of Script Evaluation in a React app: the framework walks the entire tree on the client to attach event handlers, and the cost scales with how much you rendered.
- Render less on the client. React Server Components and Next.js App Router server components never ship to the browser and never hydrate. Moving static content out of client components is the highest-leverage change available.
- Split by route and by interaction.
next/dynamicwithssr: falsekeeps a heavy widget (a map, a rich text editor, a chart) out of the initial bundle entirely. - Check what
"use client"is dragging in. A single"use client"at the top of a layout pulls its whole subtree to the client. Push the directive down to the leaves that actually need it. - Memoise the expensive renders, not everything.
useMemoandReact.memohave their own cost. Profile with the React DevTools Profiler and apply them where a flame graph shows a genuinely expensive subtree.
WordPress
The pattern is almost always plugin JavaScript loading site-wide.
- Dequeue per page. A contact form plugin, a slider, a lightbox and a social sharing script frequently load on every URL including posts that use none of them. Dequeue by template with
wp_dequeue_scriptin a smallfunctions.phpsnippet or a plugin-organiser plugin. - Delay third-party scripts. Most caching plugins (WP Rocket, LiteSpeed Cache, Perfmatters) ship a "delay JavaScript execution" option that holds non-essential scripts until first user interaction. On plugin-heavy sites this alone can halve Script Evaluation.
- Watch the page builders. Elementor, Divi and WPBakery each add their own runtime plus per-widget scripts. A page rebuilt in blocks or plain HTML routinely drops a second of main-thread work.
- Check jQuery dependants. Plugins still enqueue jQuery plus a migrate shim; if only one plugin needs it, that is a large fixed cost for a small feature.
Shopify
In a scan of 269 production storefronts, mainthread-work-breakdown failed on 75.8% of them (204 of 269), making it one of the most widely failed audits in the study.
- App scripts are the dominant cost. Reviews, upsells, loyalty programs, currency converters, wishlists and popups each inject their own bundle, and they accumulate silently as apps are trialled and abandoned. Uninstalling an app does not always remove its script tag; check the theme for orphaned includes.
- Use the theme app extensions and web pixels. Modern Shopify apps can run in a sandboxed worker via Web Pixels instead of on the main thread. Ask vendors whether their app supports it.
- Defer above the fold only what must be there. Cart drawers, quick-view modals and size guides do not need to evaluate before first paint; load them on first interaction.
- Audit the theme itself. Dawn-derived themes are light, but heavily customised themes often include a slider library, an animation library and a lazy-load library that duplicate what the platform already provides.
What main-thread pitfalls should I avoid?
- Do not optimise before reading the category split. Minifying CSS when Script Evaluation is 3.4 seconds of a 5.4 second total is a rounding error.
- Do not confuse "deferred" with "cheap".
deferandasyncchange when a script runs, not how long it takes. A deferred 400 ms script still costs 400 ms of main-thread time, just later. - Do not measure on your laptop. An unthrottled desktop run can show a quarter of the throttled mobile number. Compare like with like, or you will "fix" things that were never broken and miss the ones that are.
- Do not chase this audit at TBT's expense. The score responds to blocking time. If the work is already split into short tasks, reducing the total further has real value for battery and low-end devices but may not move the score at all.
- Do not move work to
requestAnimationFrameand call it deferred. rAF callbacks run on the main thread, before paint, in the frame budget. That is often the worst place for heavy work, not the best. - Do not assume a Web Worker is free. Transferring large structures costs serialisation time. Workers pay off for computation, not for shuttling megabytes back and forth.
How do I verify the fix?
- Re-run PageSpeed Insights on the deployed URL and compare the category table row by row, not just the total. A drop in Script Evaluation with a rise in Other usually means work moved rather than disappeared.
- In Chrome DevTools, open the Performance panel, set CPU throttling to 4x or 6x slowdown, and record a reload. The Summary donut shows the same categories, and the flame chart names the functions behind them.
- Check Total Blocking Time alongside it. Total work down and TBT flat means you removed short tasks and left the long ones, which is the wrong half.
- Confirm the improvement reaches real users: INP and the Core Web Vitals assessment in Search Console or CrUX reflect field conditions that no lab run reproduces.
Related audits
- Total Blocking Time, the scored metric that main-thread work feeds
- Reduce unused JavaScript, the usual source of Script Evaluation time
- Reduce unused CSS, which drives Style & Layout cost
- Eliminate render-blocking resources, the network-side sibling of this problem
- Largest Contentful Paint, frequently delayed by a busy main thread
Audit your URL at https://lighthouse-md.com.
Audit your page now
Paste your URL, get scores plus a CLAUDE.md plan for Claude Code.