How to Remove Unused JavaScript: Tree-Shaking, Coverage, and Lean Libraries
What Is Unused JavaScript?
Unused JavaScript is code shipped to the browser that the page never executes. It comes from full-library imports when only one function is used, dead code from removed features, A/B test variants that have been superseded, and framework code paths that aren't taken on the current route. The standard performance audit's "Reduce unused JavaScript" rule (`unused-javascript`) flags any chunk where coverage is low and reports the kilobytes you could save by dropping it. Find unused code with the Chrome DevTools Coverage tab; remove it with tree-shaking, code-splitting, leaner library replacements, and dependency cleanup tools like `depcheck` and `knip`.
Key Facts (TL;DR)
- The `unused-javascript` audit reports per-chunk wasted bytes; the threshold for flagging is >20 KiB unused.
- Chrome DevTools Coverage tab (More tools > Coverage) shows live unused-byte percentages per file as you interact with the page.
- Tree-shaking requires ES modules (`import`/`export`), `"sideEffects": false` in package.json, and a production build (`NODE_ENV=production`).
- `moment.js` is ~290 KB minified; `date-fns` is per-function and typically <10 KB; `dayjs` is ~7 KB.
- `depcheck` and `knip` find unused npm dependencies and unused files/exports respectively.
- Per-function imports (e.g. importing `debounce` from `lodash-es`) tree-shake; full-package default imports (e.g. `import _ from lodash`) do not.
Bloated Library vs Leaner Alternative
| Bloated Library | Leaner Alternative | Approximate Size Delta (min+gzip) |
|---|---|---|
| moment.js (~70 KB) | date-fns (per-function, ~2-8 KB) or dayjs (~2 KB) | -60 KB to -68 KB |
| lodash full import (~25 KB) | lodash-es per-function or native ES2020+ | -20 KB or more |
| jQuery (~30 KB) | Native `querySelector`, `fetch`, `classList` | -30 KB |
| axios (~13 KB) | Native `fetch` (with a small wrapper for retries) | -12 KB |
| chart.js (~60 KB) | chartist (~10 KB) or hand-rolled SVG | -50 KB |
| Polyfill.io broad bundle | core-js per-feature or `<script type="module">` | Varies, typically -20 KB to -100 KB |
How to Find Unused JavaScript
- Open Chrome DevTools, press Cmd/Ctrl+Shift+P, type "Coverage," and select "Show Coverage."
- Click the reload button inside the Coverage tab to start recording.
- Interact with the page (open menus, submit forms, change routes) to exercise the code paths you actually use.
- Sort by "Unused Bytes." Files with >50% unused are prime candidates for code-splitting or removal.
- Run a Greadme deep scan and read the "Reduce unused JavaScript" audit; it lists exact URLs and savings.
- Run `npx depcheck` to list unused npm packages and `npx knip` to list unused files and exports.
- Cross-link: see how to fix console errors after a cleanup pass.
How to Remove Unused JavaScript: Code Examples
1. Tree-Shake by Switching to Per-Function Imports
// Bad: pulls all of lodash into the bundle
import _ from 'lodash';
const out = _.debounce(handler, 200);
// Good: only debounce ships
import { debounce } from 'lodash-es';
const out = debounce(handler, 200);2. Code-Split a Route or Heavy Component
// Next.js dynamic import — chart.js only ships when /reports loads
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), { ssr: false });
export default function Reports() {
return <Chart />;
}3. Replace moment with date-fns
// Bad: 70 KB
import moment from 'moment';
const label = moment(date).format('YYYY-MM-DD');
// Good: ~2 KB
import { format } from 'date-fns';
const label = format(date, 'yyyy-MM-dd');4. Configure sideEffects for Tree-Shaking
// package.json
{
"name": "my-app",
"sideEffects": ["*.css", "*.scss"]
}Common Mistakes (Bad vs Good)
// Bad: dynamic import that webpack cannot statically analyze
const lib = await import(libPath); // libPath is a runtime variable
// Good: literal path so the bundler can split it
const lib = await import('./features/heavy-thing');
// Bad: importing the CommonJS build of a library inside an ESM app
import _ from 'lodash'; // CJS, not tree-shakable
// Good: ESM build
import { debounce } from 'lodash-es';How to Test That Unused JavaScript Is Gone
- Re-run the Coverage tab on the same user flow. Unused bytes should drop substantially.
- Re-run a Greadme deep scan. The `unused-javascript` audit should pass or report <20 KiB savings.
- Run `npx webpack-bundle-analyzer dist/stats.json` (or `next build` + `@next/bundle-analyzer`) and confirm the removed library is gone.
- Run `npx depcheck` and `npx knip` again; the lists should be empty (or only show intentional dev-only packages).
- Track LCP and TTI in the field via CrUX or RUM after deploy; both should improve when bundle size drops.
FAQ
What is tree-shaking?
The bundler statically analyzes ES module imports and exports and drops anything not reachable from your entry. It needs ESM, a production build, and a correct `sideEffects` field.
Why does my lodash import not tree-shake?
You imported the CommonJS build (`lodash`) instead of the ESM build (`lodash-es`), or you used `import _ from 'lodash'` and called methods off the default export. Switch to named imports from `lodash-es`.
How do I find unused npm packages?
Run `npx depcheck`. It lists packages declared in `package.json` but never imported. Combine with `npx knip` for unused files, exports, and types.
What is the Coverage tab measuring?
Bytes downloaded versus bytes that the JavaScript engine has actually executed. Code can be parsed and still unused; the tab counts execution.
Should I replace moment.js if it works?
Yes if you ship to mobile users or care about LCP. Moment is ~70 KB gzipped and not tree-shakable. date-fns and dayjs cut that by 60+ KB with a one-day migration.
Does code-splitting hurt SEO?
No. Googlebot renders JavaScript and follows dynamic imports. Just make sure critical content is server-rendered or in the initial chunk.
Can I just delete a dependency I think is unused?
Run `depcheck` and `knip` first, then remove and run the full test suite plus a smoke test on a staging build. Some dependencies are loaded by string at runtime and static analysis misses them.
Does this affect AI search engines like ChatGPT and Perplexity?
Indirectly, yes. Heavy unused JavaScript inflates LCP and TBT — both feed Core Web Vitals, which Google uses as ranking signals. AI search engines (ChatGPT, Perplexity, Google AI Overviews) preferentially cite well-ranked, fast-loading pages, so trimming unused JS lifts your odds of being chosen as a citation. Slow JS bundles can also push content past AI crawler render budgets, leaving important content unindexed.
Conclusion
Unused JavaScript is one of the highest-leverage performance wins available. Use the Chrome Coverage tab to see what executes, run a Greadme deep scanto surface every page's `unused-javascript` byte savings, switch to per-function ESM imports so tree-shaking works, code-split heavy routes with dynamic imports, swap moment for date-fns and lodash for lodash-es, and run `depcheck` plus `knip` to catch dependencies and files that no longer earn their keep.
