Most production sites ship three to five times more JavaScript than they need. The audit that finds the dead weight takes about thirty minutes. The savings show up immediately in LCP and INP on real-user data — not in a Lighthouse score from a Chicago datacenter, but in the field numbers you actually have to defend.
The audit setup
Pick the analyzer that matches your build tool. The three good ones are rollup-plugin-visualizer for Rollup, vite-bundle-visualizer for Vite, and webpack-bundle-analyzer for Webpack and Next.js. They all produce the same shape of output: a treemap where every rectangle is a module and the area is proportional to the bytes it contributes.
For a Vite project, the config is three lines:
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({ open: true, gzipSize: true, brotliSize: true }),
],
});Run a production build. The visualizer opens in a browser tab. You are now looking at every kilobyte of JavaScript your users download. Toggle to gzipped sizes — that is what they pay for over the network. Uncompressed sizes are what they pay for in parse and compile time on the device.
The five typical offenders
1. Icon libraries imported in full
Open the treemap. If you see a rectangle for @heroicons, lucide-react, or react-icons bigger than 40kb gzipped, you have a wildcard import somewhere. The pattern that ships the whole library is:
// Ships every icon in the library import * as Icons from 'lucide-react'; <Icons.Search /> // Or sometimes hidden behind a re-export barrel: // icons/index.ts export * from 'lucide-react';
Replace with named imports of the eight icons you actually use:
import { Search, Menu, X, ChevronDown,
User, Settings, LogOut, Plus } from 'lucide-react';2. Moment.js
Moment.js is roughly 290kb minified, including locale data for every language. If your app supports two languages, you are shipping locale files for 600 others. Modern replacements:
date-fnsimported as named functions — a typical app usesformat,parseISO,formatDistance, and lands around 25–35kb.dayjsat around 7kb if you need a chainable API.- Native
Intl.DateTimeFormatat zero kilobytes if all you needed was "display this date in the user's locale".
3. Lodash full
The full lodash package is about 70kb gzipped. Almost nobody needs all of it. The two upgrades:
// Bad: ships everything import _ from 'lodash'; _.debounce(fn, 300); // Better: specific import paths, tree-shakable import debounce from 'lodash/debounce'; // Best: most lodash methods have a native equivalent now const grouped = Object.groupBy(items, item => item.category);
Object.groupBy, Array.prototype.flat, structuredClone, and the spread/rest operators cover the lodash methods most apps actually used.
4. Polyfills for browsers you do not support
Default core-js configurations bundle polyfills for ES5 targets, IE11, and ancient Safari versions. Check your browserslist. If it says defaults, it probably includes browsers that have not received updates in six years. Tighten it to > 0.5%, last 2 versions, not dead and watch the regenerator-runtime rectangle disappear.
5. Heavy visualisation libraries
recharts, victory, and chart.js weigh between 80 and 200kb each. They are wonderful when you need interactive charts with rich tooltips. For a single line chart on a dashboard card, a 60-line SVG component with d3-scale alone is enough. If you do need a chart library, lazy-load the route it lives on so it is not in the initial bundle.
How to read the visualizer
Square area equals bytes. Click into a vendor chunk to expand it into its constituent packages. Hover for the exact gzipped and uncompressed sizes. The big squares are the ones to investigate first — a 4kb dependency is not worth your time even if it is a completely unused import. Two rules of thumb:
- Gzipped size is the network cost. That is what shows up in TTFB and download time.
- Uncompressed size is the device cost. Parse, compile, and execute scale with uncompressed bytes, and on a mid-range Android the device cost dominates.
A route-based code-split build will show you separate bundles per route. The one you care about most is the entry bundle that loads on the landing route — that is the one users wait for on a cold visit.
The replacement strategy
One change at a time, measured. The temptation is to delete five things in a single PR. Resist it. If LCP improves by 600 ms, you want to know which of the five was responsible — partly to tell the next team, partly because one of the five might have regressed something subtler.
The loop:
- Capture the current bundle sizes per route and the field LCP/INP numbers.
- Ship one replacement.
- Wait 48 hours for field data to settle.
- Compare. Note the delta. Move on to the next offender.
What savings look like in real numbers
A typical SPA audit on a six-month-old codebase: initial bundle drops from 280kb gzipped to 110kb. On 4G with a 200 ms RTT, that is roughly 600 ms off the download phase of LCP and a measurable INP improvement because the main thread is no longer parsing 170kb of dead code during hydration. The numbers are not theoretical — they are what the audit produces when nobody on the team has done one in a year.
What not to do
Two failure modes to avoid:
- Micro-optimising small dependencies. If a library is 3kb, leave it alone. The win is not worth the review cost.
- Replacing a 5kb library with custom code you will maintain forever. Date arithmetic, slug generation, deep equality — these are the libraries where the maintenance cost of your replacement exceeds the bytes you save in five releases.
The habit that compounds
Every team has 100kb of dead JavaScript to delete and the work to find it is a few hours. This is the highest-leverage frontend performance work that exists. Schedule a bundle audit once a quarter; the dependencies your team adds in between will produce fresh offenders, and catching them while they are still small is how you avoid the once-a-year emergency rewrite.
Related reading
If you are not yet sure which metric your bundle is hurting most, start with the Core Web Vitals working definition. For the deeper architectural lever — paying less JavaScript on hydration, not just at download — see the hydration tax guide. Both sit inside the web performance topic.
About the writers
Founder of ShareCode. Writes the engineering deep-dives on this site — WebRTC, Firebase Auth, real-time sync, and the production patterns behind the editor itself.
More from Kishan
Developer educator at ShareCode. Writes the tutorial track — Python, JavaScript debugging, coding-interview prep, and the everyday code-quality habits that hold up in real codebases.
More from Kajal
Running the audit this week?
Paste your visualizer output and the names of your top five vendor packages into a code space, share the link with a teammate, and pick the first offender to delete together. Most audits identify the first 50kb of dead weight in the first ten minutes of pairing.
Open a code space →