How to Reduce Unused JavaScript
Your page ships JavaScript the browser downloads and parses but often never runs on that page. This wastes bandwidth and main-thread time, delaying LCP and interactivity. The fix is to load less JS up front: code-split, defer, tree-shake, and remove dead scripts and unused third-party tags.
What this means
The excess_unused_js warning means the audit found JavaScript being delivered to the page that isn't needed to render what the visitor actually sees, at least not immediately. Chrome's coverage tooling (the same data Lighthouse uses) measures how many bytes of each script are executed versus downloaded. When a large share of your bundled or third-party JS goes unused on load, it gets flagged.
Common culprits: one large bundle that includes code for every page of the site (no code-splitting); third-party tags like chat widgets, A/B testing tools, analytics, and ad scripts that load site-wide; unused portions of libraries (importing an entire utility library to use one function); and plugins or theme features you no longer use.
The cost isn't just the download. Every kilobyte of JavaScript has to be parsed and compiled on the browser's main thread, which is far more expensive per byte than an image. On mid-range mobile devices, this parse-and-compile work is often what delays the page becoming interactive.
Why it matters
Unused JavaScript hurts Core Web Vitals, which are part of Google's page experience ranking signals. Large scripts delay the browser from painting your Largest Contentful Paint (LCP) element, and heavy main-thread work worsens Interaction to Next Paint (INP). Google's "good" thresholds are LCP under 2.5 seconds and INP under 200 milliseconds at the 75th percentile, and excess JS is a common reason pages miss them on mobile.
Beyond the ranking signal, script-heavy pages raise bounce rate and cut conversions. The visitor stares at a blank or unresponsive screen while the CPU chews through code they may never trigger.
There's an AI-visibility angle too. Many AI crawlers execute little or no JavaScript. If your core content only appears after a heavy bundle runs, a non-rendering crawler can see an empty shell. Serving content in the initial HTML (server-side rendering or static generation) makes it reliably visible to Googlebot and to answer engines like Google's AI Overviews, ChatGPT, and Perplexity. Trimming bundles and moving toward server-rendered content helps you on both fronts at once.
How to fix it
- 1
Find exactly which scripts are unused
Open Chrome DevTools, press Ctrl+Shift+P (Cmd+Shift+P on Mac), type "Coverage," and choose "Show Coverage." Reload the page and read the bars: the red portion is unused bytes. Sort by URL to see which bundles and third-party tags waste the most, so you focus instead of guessing. Cross-check against the "Reduce unused JavaScript" item in a Lighthouse or PageSpeed Insights run, which lists the same files with estimated savings.
- 2
Defer non-critical JavaScript
Any script not needed to paint above-the-fold content should not block rendering. Add the
deferattribute to your own scripts so they download in parallel and run after HTML parsing. Useasyncfor independent third-party tags like analytics. Load tags that only matter after interaction (chat, surveys, some ad scripts) on first user interaction or when the page is idle. This doesn't remove the bytes, but it stops them from delaying LCP. - 3
Code-split so each page loads only its own JS
The biggest single win on most sites is splitting one giant bundle into per-route chunks, so a visitor to a blog post doesn't download the checkout code. Modern bundlers (webpack, Vite, esbuild, Rollup) do this with dynamic
import(). Frameworks like Next.js, Nuxt, and Remix code-split by route automatically and support lazy loading for heavy components (charts, maps, video players) that aren't visible on load. Pair this with tree-shaking: import only the functions you use, for exampleimport debounce from 'lodash/debounce'instead of the whole library. - 4
Audit and cut third-party tags
Third-party scripts are often the largest chunk of unused JS, and you don't control their internals, so the lever is deciding whether each tag earns its place. In Google Tag Manager or your tag setup, remove tags you no longer use and set triggers so tags fire only on pages that need them rather than site-wide. Load a chat widget or heatmap tool only where it matters or on first interaction. Each removed vendor script removes its full download, parse, and execution cost.
- 5
On WordPress, dequeue per-page plugin assets and defer via a caching plugin
Many WordPress plugins enqueue their JavaScript on every page even when the feature appears on only one. Use Perfmatters or Asset CleanUp to dequeue scripts on pages that don't use them (for example, disable a contact-form script everywhere except the contact page). A caching or optimization plugin such as WP Rocket, LiteSpeed Cache, or the free FlyingPress can defer JavaScript and delay non-critical scripts until interaction. Delete plugins you've deactivated but left installed, and prefer lightweight themes that don't load a page builder's full runtime everywhere.
- 6
Re-test and confirm the savings
After each change, re-run PageSpeed Insights and re-check the Coverage tab to confirm unused bytes dropped and LCP and INP improved. Test on a throttled mobile profile, since that's where excess JS hurts most and how Google's field data is weighted. Deferring or removing a script can break functionality, so click through interactive elements (menus, forms, sliders) before shipping. Lab tools confirm the fix immediately; the Core Web Vitals report in Search Console uses a rolling 28-day window, so field metrics catch up over time.
Example
<!-- BEFORE: blocking, ships on every page, runs before the page can render -->
<script src="/js/app.bundle.js"></script>
<!-- AFTER: defer non-critical JS so it doesn't delay LCP -->
<script src="/js/app.bundle.js" defer></script>
<script src="https://www.googletagmanager.com/gtag/js?id=GA_ID" async></script>
<!-- Code-split: load a heavy widget only when it enters the viewport -->
<div id="map" data-map></div>
<script type="module">
const el = document.querySelector('[data-map]');
const io = new IntersectionObserver(async (entries) => {
if (entries[0].isIntersecting) {
const { initMap } = await import('/js/map.js'); // separate chunk
initMap(el);
io.disconnect();
}
});
io.observe(el);
</script>Defer your own scripts and lazy-load a heavy component only when it's needed, instead of shipping everything in one blocking bundle.
Platform-specific steps
Yoast and Rank Math handle metadata, not asset loading, so they won't fix this directly. Add Perfmatters or Asset CleanUp to dequeue plugin scripts on pages that don't use them, and use WP Rocket, LiteSpeed Cache, or FlyingPress to defer JavaScript and delay non-critical scripts until user interaction. Delete deactivated plugins and switch off theme or page-builder features you don't use.
Audit installed apps first: many inject JavaScript on every page even after you stop using them, so uninstall dead apps rather than just disabling them. Move third-party pixels and tags into Shopify's Custom Pixels (Settings > Customer events) so they run in a sandbox instead of on the main thread. Choose a lightweight Online Store 2.0 theme and remove unused theme app embeds under Themes > Customize > App embeds.
Wix controls its own bundling, so your main levers are removing unused third-party embeds and apps from the Wix App Market and avoiding heavy custom Velo code that runs on every page. Remove tracking tags you no longer need under Settings > Custom Code, and scope any remaining ones to specific pages rather than the whole site.
Squarespace bundles its platform JavaScript for you, so focus on what you add: remove unused code injections (Settings > Advanced > Code Injection), delete third-party embed blocks and tracking scripts you no longer use, and scope injected scripts to the specific pages that need them via page-level code injection rather than site-wide.
Next.js code-splits by route automatically; use next/dynamic with { ssr: false } for heavy client-only components (charts, editors, maps) so they load in a separate chunk on demand. Import specific functions instead of whole libraries to enable tree-shaking, run @next/bundle-analyzer to find bloated chunks, and load third-party tags with next/script using the afterInteractive or lazyOnload strategy. For raw HTML, split bundles with dynamic import() and add defer or async to script tags.
Frequently asked
There's no fixed byte limit. Lighthouse flags a file when it estimates you could save a meaningful amount of load time by not sending its unused portion, roughly starting around 20 KB of wasted transfer per file. Focus less on the exact number and more on the biggest offenders in the Coverage report and on whether your LCP is under 2.5 seconds on mobile. If it is, small amounts of unused JS aren't worth chasing.
It can if a script depends on running before the DOM finishes parsing or before another script. defer preserves execution order among deferred scripts and runs them after parsing, which is safe for most site scripts. async runs as soon as it downloads, so use it only for independent tags like analytics. Test interactive features after each change; if something breaks, that specific script probably needs to stay non-deferred or load differently.
They overlap but aren't identical. Render-blocking JavaScript is a script in the <head> without defer or async that stops the browser from rendering until it loads and runs. Unused JavaScript is code that's downloaded but never executed on that page, whether or not it blocks rendering. A script can be both, one, or neither. Deferring fixes the render-blocking part; code-splitting and removing tags fixes the unused part.
Indirectly, yes. Many AI crawlers execute little or no JavaScript, so if your main content only renders after a heavy bundle runs, those crawlers may see an empty page. Serving content in the initial HTML through server-side rendering or static generation makes it reliably visible to AI answer engines and to Googlebot. Trimming unused JS is a natural byproduct of moving toward server-rendered content.
Page builders load a large runtime and multiple widget scripts on every page, much of which a given page never uses. Enable the builder's own performance features (in Elementor, "Improved Asset Loading" and "Optimized DOM Output"), use an asset-management plugin to dequeue unused widget scripts per page, and defer scripts through a caching plugin. For pages that don't need the builder at all, consider building them with the native block editor.
Does your site have this issue?
Run a free, AI-powered audit and we’ll flag this and 150+ other checks in about a minute. No signup.