← All articles
📦 PerformanceIntermediate · 12 min read

Auditing a JavaScript Bundle: The Largest Single Lever

A 30-minute audit that, on almost every production site, finds 100kb of JavaScript you can delete without breaking anything.

By Kishan Vaghani · Reviewed by Kajal Pansuriya · Published May 26, 2026

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:

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:

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:

  1. Capture the current bundle sizes per route and the field LCP/INP numbers.
  2. Ship one replacement.
  3. Wait 48 hours for field data to settle.
  4. 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:

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

Author

Kishan Vaghani

Founder & Lead Engineer, ShareCode

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.

Real-time collaboration & CRDTsWebRTC & low-latency mediaFirebase authentication & security rulesNext.js & full-stack JavaScript
Reviewed by

Kajal Pansuriya

Developer Educator, ShareCode

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.

Python fundamentals & teaching beginnersJavaScript debugging & DevToolsCoding-interview preparationClean code & code review

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