What Is Unused JavaScript? Complete Guide (2026)
What Is Unused JavaScript?
Unused JavaScript is JS code that the browser downloads, parses, and compiles on every page load but never actually executes. Performance audits flag a script when its unused bytes exceed roughly 20 KB or 50% of the file — at that point the cost of shipping it outweighs any benefit.
Key Facts (TL;DR)
- Audit threshold: Flagged when > 20 KB or > 50% of a script's bytes are unused.
- Industry baseline: The median page ships 35–40% unused JavaScript at the 50th percentile (HTTP Archive Web Almanac, JavaScript chapter).
- Parse cost: Every kilobyte of JS costs roughly ~1 ms of parse + compile time on a mid-range mobile CPU. A 1 MB bundle that's 50% unused wastes ~500 ms of main-thread time on every load.
- Affected metrics: Total Blocking Time (TBT), Interaction to Next Paint (INP), and Largest Contentful Paint (LCP) — all degrade when the main thread is busy parsing dead code.
- Top causes: Importing entire libraries when only one function is needed, polyfills for browsers that don't need them, and all-routes-in-one-bundle apps.
Think of unused JavaScript like packing your entire kitchen into the car for a weekend trip. You still pay the gas to haul it, the time to load it in, and the time to unload it — but you only ever cook one meal. Every kilobyte of unused JS is luggage the browser carries for nothing.
Why Unused JavaScript Hurts Performance and Rankings
JavaScript is the most expensive byte on the web. Unlike images or CSS, JS goes through four stages — download, parse, compile, execute — and unused code pays the first three of those costs in full. Concretely, unused JS:
- Blocks the main thread.Parse and compile run synchronously. While the browser is chewing on a 500 KB bundle that's half dead code, it can't respond to taps or paint the LCP element.
- Inflates Total Blocking Time and INP. Both metrics measure main-thread availability. More unused JS = more long tasks = worse scores.
- Slows Largest Contentful Paint. If the LCP element is rendered by JavaScript (a common pattern in single-page apps), every millisecond of parse cost delays the paint.
- Wastes mobile data and battery. Mobile CPUs parse JS 2–4× slower than desktops. Industry analysis consistently shows mobile parse cost dominates total blocking time on mid-range Android devices.
- Drags down rankings. Core Web Vitals (LCP, INP, CLS) are confirmed Google ranking signals. A bundle that wastes 300+ ms of main thread tends to fail INP at the 75th percentile.
Where Unused JavaScript Comes From
Almost every site accumulates unused JS through the same handful of patterns. Identifying which of these apply to your codebase tells you which fix to reach for.
- Whole-library imports.Importing all of a utility library when you only need one function. The bundler can't always tree-shake what looks like a side-effecting CommonJS module.
- Polyfills shipped to modern browsers. Older toolchains transpile ES2020+ down to ES5 and ship the polyfills to every visitor — including the 95% on evergreen browsers.
- Single-bundle architectures. A SPA that bundles every route into one file forces homepage visitors to download checkout, settings, admin, and dashboard code they may never see.
- Dead code from old features. Components, A/B test arms, and feature-flagged paths that never got removed after the experiment ended.
- Vendor bundles that bypass tree-shaking. A single CommonJS dependency in a chain can poison tree-shaking for the whole vendor chunk.
- Third-party tags.Analytics, chat, and consent scripts often include features you don't use — and they're hard to tree-shake because they're loaded as opaque blobs.
Worked example: a single bad import
The classic offender is a library import that pulls in the whole package instead of the single function you need:
// Bad: pulls the entire library into your bundle (~70 KB)
import _ from 'lodash';
const debounced = _.debounce(handler, 250);
// Good: pulls only the function you use (~2 KB)
import debounce from 'lodash/debounce';
const debounced = debounce(handler, 250);
// Also good: tree-shakable ESM build
import { debounce } from 'lodash-es';That single change can cut a vendor chunk from ~70 KB to ~2 KB — a 35× reduction for one utility. Multiply that across a real codebase and the savings are in the hundreds of kilobytes.
How to Measure Unused JavaScript
Unused JS shows up in two places: synthetic audits (lab data) and real-user metrics (field data). You want both — synthetic tells you which scripts are bloated; field tells you which pages actually suffer.
- Greadme deep scan— flags every script with > 20 KB or > 50% unused bytes, identifies the offending file, and pairs each finding with an AI-generated fix or a one-click GitHub PR.
- Greadme crawler scan — measures unused JS across every indexable page so you can see which routes or templates carry the most dead code.
- Greadme AI visibility analyzer — checks whether your bundle weight is hurting the CWV scores that AI search engines use as a citation signal.
- Chrome DevTools → Coverage tab— open DevTools, run "Show Coverage" from the command menu, reload the page, and you'll see exactly which lines of every script ran. Anything red is unused on that page load.
- Google Search Console → Core Web Vitals report — shows real-user INP and LCP at the 75th percentile. If those are failing, unused JS is usually a contributor.
- web.dev measure — runs a synthetic audit and lists every script with significant unused bytes, with the savings in KB.
8 Proven Ways to Eliminate Unused JavaScript
1. Import Only What You Use
The single highest-leverage fix on most codebases is replacing whole-library imports with named or path imports.
// Bad
import * as Icons from 'react-icons/fa';
<Icons.FaCheck />
// Good
import { FaCheck } from 'react-icons/fa';
// Also good — direct path import
import FaCheck from 'react-icons/fa/FaCheck';2. Code-Split Per Route
Frameworks like Next.js, Nuxt, SvelteKit, and Astro split JavaScript per route automatically. If you're hand-rolling, use the bundler's entry-point splitting (webpack, Rollup, Vite, esbuild, Parcel all support it).
Result:A homepage visitor downloads only the homepage's JS, not the entire app.
3. Use Dynamic Imports for Rarely-Used Features
Modals, charts, rich-text editors, and admin panels typically don't need to be in the initial bundle.
// Static import: ships in main bundle
import Modal from './Modal';
// Dynamic import: ships only when the user opens the modal
button.addEventListener('click', async () => {
const { default: Modal } = await import('./Modal');
Modal.open();
});4. Tree-Shake With ES Modules
Tree-shaking only works on ES module syntax (import/export). CommonJS (require) is opaque to bundlers. Pick the ESM build of every dependency that offers one, and set "sideEffects": false in your own package's package.json when safe.
5. Drop Polyfills With module/nomodule
Modern browsers don't need polyfills. Ship two bundles: a small modern one to evergreen browsers, a larger transpiled one to legacy.
<!-- Modern browsers download this -->
<script type="module" src="/app.modern.js"></script>
<!-- Only legacy browsers download this -->
<script nomodule src="/app.legacy.js"></script>Modern build targets (target: 'es2020' or higher) plus the module/nomodule pattern typically save 20–40 KB of polyfills per visit.
6. Replace Heavy Libraries With Lighter Ones
Many ecosystem libraries have drop-in replacements an order of magnitude smaller. Examples: native Intl.DateTimeFormat instead of a date library, fetch instead of an HTTP wrapper, native CSS instead of a JS animation library, Intl.NumberFormat for currency formatting.
7. Audit Third-Party Scripts
Tag managers, chat widgets, A/B testing scripts, and analytics tags accumulate over time. Quarterly: pull the live list, ask each tag's owner whether it's still needed, and remove the ones that aren't. Defer the rest with async or deferso they don't block the main thread.
8. Set a Bundle Budget in CI
Prevention beats cleanup. Add a hard cap to your build (most bundlers support performance.maxAssetSize) so a PR that pushes the main bundle over the budget fails CI.
// webpack.config.js
module.exports = {
performance: {
hints: 'error',
maxAssetSize: 200_000, // 200 KB per chunk
maxEntrypointSize: 250_000, // 250 KB initial entry
},
};Common Unused-JS Problems and Fixes
Problem: A Single Big Vendor Chunk
What's happening: Every dependency is bundled into one vendor.js that loads on every page, even pages that don't use most of it.
Fix: Switch to per-route splitting and let the bundler decide which deps to extract. Modern bundlers (webpack's splitChunks, Vite, Rollup, esbuild) handle this automatically once you stop forcing a single vendor entry.
Problem: Polyfills Shipped to Modern Browsers
What's happening: The build targets ES5, so every visitor — including the 95% on evergreen browsers — downloads polyfills for features their browser already supports.
Fix: Raise the build target to ES2020 (or higher), use module/nomodule if you must support legacy. Typical saving: 20–40 KB.
Problem: Tree-Shaking Doesn't Work
What's happening: A CommonJS dependency in the chain forces the bundler to include the whole module. Or a library marks itself as having side effects.
Fix: Prefer the ESM build of each dependency (most publish both). Mark your own package as "sideEffects": false in package.json when safe — list specific files (CSS imports, polyfills) that do have side effects.
Problem: Third-Party Tag Bloat
What's happening: Marketing and analytics scripts add up — each one ships its full SDK even if you only use one feature.
Fix: Audit tags quarterly. Defer everything non-critical with async/defer. Move tags into a tag manager so non-engineers can remove obsolete ones without a deploy.
Unused JavaScript Thresholds at a Glance
Synthetic audits use a fixed threshold to decide when unused JS is worth flagging. The rule of thumb:
| Status | Unused bytes | Unused % | What it means |
|---|---|---|---|
| Good | < 20 KB | < 30% | Bundle is reasonably tree-shaken; not worth chasing further on this script. |
| Needs improvement | 20–50 KB | 30–50% | One or two whole-library imports or a polyfill bundle; usually a quick fix. |
| Poor | > 50 KB | > 50% | Audits flag this; main thread cost is meaningful on mobile. Code-split or replace the dependency. |
Industry analysis (HTTP Archive Web Almanac) shows the median page sits in the "Needs improvement" band — most sites ship 35–40% unused JS at p50.
FAQ
What counts as "unused" JavaScript?
Any JS that's downloaded, parsed, and compiled but never executed during the page load. Synthetic audits measure this by instrumenting the script and counting which lines ran. A function that's defined but never called is unused. A whole library you import but only use one method from is mostly unused.
How much unused JavaScript is too much?
Audits flag a script when unused bytes exceed 20 KB or 50% of the file. In practice, the cost becomes painful (visible INP and LCP regressions on mobile) once the unused portion of your initial bundle exceeds ~100 KB.
Does unused JavaScript affect SEO?
Yes, indirectly. Unused JS slows the main thread, which worsens INP and LCP — two of Google's three Core Web Vitals and confirmed ranking signals. Pages with poor CWV at the 75th percentile lose ranking ground in competitive queries.
Why is the median page still 35–40% unused JS?
Three reasons: most sites still bundle every route together, most apps still ship polyfills to modern browsers, and most teams import whole libraries instead of named methods. Each of those is a fix any engineer can make in an afternoon — but they require active attention, and bundle size only gets measured when something breaks.
Will tree-shaking remove all unused JS automatically?
Only if every link in the dependency chain uses ES modules and declares its side effects correctly. A single CommonJS package or a missing "sideEffects": false can force the bundler to keep code it would otherwise drop. Tree-shaking is necessary but not sufficient — code splitting and dynamic imports do the rest.
Does unused JavaScript affect AI search engines like ChatGPT and Perplexity?
Indirectly, yes. Generative search systems most often cite pages that already rank well in traditional search — and Core Web Vitals are part of that ranking signal. Google AI Overviews preferentially surfaces pages that pass CWV. A bundle bloated with unused JS pushes INP above the 200 ms threshold, hurts your rankings, and reduces your odds of being chosen as a citation.
How do I find unused JS without instrumenting the browser myself?
Run a Greadme deep scan on the page — it surfaces every script with significant unused bytes, and pairs each finding with an AI-generated fix. For a manual check, Chrome DevTools' Coverage tab shows used vs. unused bytes line-by-line; Google Search Console's Core Web Vitals report tells you if real users are feeling the pain.
Conclusion
Unused JavaScript is the single most expensive form of dead weight on the web. Every kilobyte costs roughly a millisecond of mobile parse time, blocks the main thread during the most critical part of the page load, and drags down INP and LCP — the two metrics that decide whether you pass Core Web Vitals.
The fixes are well understood: import only what you use, code-split per route, dynamically import rarely-used features, drop polyfills for modern browsers, and put a bundle budget in CI so the next regression fails the build instead of the user. Start by running a Greadme deep scan to see exactly which scripts on your site cross the 20 KB / 50% threshold, then fix them in order of bytes saved.
