Cutting JavaScript bundle size doesn't require a rewrite. Learn how to audit your dependencies, tree-shake dead code, and optimize your build for speed.

We’ve all been there: you push a "minor" feature update, only to notice your LCP (Largest Contentful Paint) creeping toward the three-second mark. I recently inherited a dashboard that felt sluggish on mobile, and the culprit was a massive, bloated vendor chunk.
Cutting JavaScript bundle size is often less about rewriting your core logic and more about surgical removal of the bloat you didn't know you were shipping. Here is how I approached the problem without burning down the codebase.
You can't fix what you can't see. I started by running webpack-bundle-analyzer on the production build. It’s a standard tool, but the insight it provides is transformative. Seeing a massive block representing a library I only used for one helper function was a wake-up call.
If you’re using Webpack, add the plugin to your config:
JAVASCRIPTconst BundleAnalyzerPlugin = require(CE9178">'webpack-bundle-analyzer').BundleAnalyzerPlugin; module.exports = { plugins: [ new BundleAnalyzerPlugin() ] }
Once the report generated, I saw two major offenders: moment.js and a massive icons library. We were using moment to format a single date in the footer, which accounted for roughly 70KB of gzipped code. Replacing it with date-fns dropped that down to about 4KB. It wasn't a rewrite; it was a targeted replacement.
Even if you don't use a library, it might still end up in your bundle. Tree-shaking relies on ES modules (import/export) to prune dead code. If your dependencies use CommonJS (module.exports), the bundler often assumes the entire package is needed.
I noticed a utility library we were using was being bundled in its entirety. I switched to importing only the specific functions:
JAVASCRIPT// Bad: imports the whole library import _ from CE9178">'lodash'; // Better: imports only the chunked function import debounce from CE9178">'lodash/debounce';
This simple change saved us around 120KB. If you’re struggling with types while refactoring your imports, remember that TypeScript narrowing: How to make the compiler trust your code helps keep your refactored logic predictable. When you start splitting things up, you’ll also want to ensure your component structure remains efficient, which is why understanding Lists and keys in React: Why the console warnings matter is vital to keep your UI updates fast after you've optimized the load.

I initially tried to move everything to dynamic import() statements to defer loading. It worked for the heavy charting library, but it introduced a nasty "layout shift" when the component finally hydrated. I had to implement a loading skeleton to mask the delay, which ended up being more work than just swapping the heavy library for a lighter alternative.
Sometimes, the best optimization is simply deleting a dependency you thought you needed. I found a package that was only being used to capitalize strings—a native JavaScript function. I nuked the dependency, added a one-line helper, and felt zero regret.
To keep your bundle lean long-term, consider these rules:
npm install becomes a habit.sideEffects: false so your bundler knows it’s safe to prune unused exports.
I’m still not entirely happy with how we handle our third-party analytics scripts. They are external, so they don't show up in my bundle analyzer, but they are clearly impacting the main thread during execution. I’m currently experimenting with loading them via a web worker, but that’s a rabbit hole for another day.
Don't aim for a perfectly tiny bundle on the first pass. Focus on the low-hanging fruit—the massive libraries that provide marginal value. Once you start pruning, you’ll develop a better intuition for what’s worth keeping.
Fixing LCP is often simpler than it looks. Learn how to identify the real bottlenecks, optimize hero images, and stop wasting time on minor tweaks.