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.
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.
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:
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.
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.
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.
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';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.
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();
});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.
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.
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.
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.
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
},
};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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.