DEV Community

Royce
Royce

Posted on • Edited on • Originally published at pkgpulse.com

I Cut a React App's Bundle from 450KB to 120KB. Here's How.

Every kilobyte of JavaScript you ship has a cost. It needs to be downloaded, parsed, compiled, and executed — and on a budget Android phone over a 4G connection, that cost compounds fast.

Amazon found that every 100ms of load time costs 1% in revenue. Google's Core Web Vitals directly penalize large bundles. And in 2026, with mobile-first indexing and increasingly impatient users, bundle size isn't a nice-to-have optimization. It's a competitive advantage.

Here's a practical guide to finding and fixing bundle bloat, with real package size data from PkgPulse.

Why Bundle Size Matters More Than Ever

When a browser downloads your JavaScript, it doesn't just download it. It has to:

  1. Download — connection-dependent (3G: ~400KB/s, 4G: ~4MB/s)
  2. Parse — read and validate JavaScript syntax
  3. Compile — V8/SpiderMonkey compiles to machine code
  4. Execute — initialization, event listeners, framework bootstrap

Steps 2-4 are CPU-bound. A 500KB bundle that loads in 200ms on your MacBook Pro might take 2-3 seconds on a mid-range phone. Your users feel that.

The SEO Impact

Google's Core Web Vitals measure three things, and bundle size affects all of them:

  • LCP (Largest Contentful Paint) — large bundles delay initial render
  • INP (Interaction to Next Paint) — heavy JS blocks the main thread, making the UI feel sluggish
  • CLS (Cumulative Layout Shift) — late-loading JS causes layout jumps

Sites that fail Core Web Vitals rank lower. Bundle size is an SEO problem disguised as a performance problem.

Step 1: Measure Before You Optimize

You can't fix what you can't see. Before changing anything, understand what's in your bundle.

Analysis Tools

  • webpack-bundle-analyzer — interactive treemap of your entire bundle
  • source-map-explorer — source map-based analysis showing what takes space
  • @next/bundle-analyzer — Next.js wrapper
  • PkgPulse — compare package sizes before you install them

What to Look For

When you run a bundle analyzer, flag these patterns:

  1. One package dominating — a single dependency taking 30%+ of your bundle
  2. Duplicate code — multiple versions of the same package (common with transitive deps)
  3. Dead imports — packages imported but only partially used
  4. Locale bloat — date/time libraries bundling every locale by default

Step 2: Swap Heavy Dependencies for Lighter Ones

The biggest wins come from replacing heavy packages with lighter alternatives. This is where PkgPulse earns its keep — compare sizes before you commit to an npm install.

High-Impact Swaps

Heavy Package Size (gzip) Lighter Alternative Size (gzip) Savings PkgPulse
moment ~72KB dayjs ~2KB 97% Compare
lodash ~71KB lodash-es (tree-shake) varies 60-90% Compare
axios ~14KB ky ~3KB 78% Compare
chalk ~5KB picocolors ~1KB 80% Compare
uuid ~3KB crypto.randomUUID() 0KB 100% Built-in

Moment to Day.js: The Easiest Win

This single swap saves 70KB for most projects. Moment.js loads every locale by default. Day.js provides the same API in 2KB, with locale plugins loaded on demand.

// Before: moment (72KB gzipped)
import moment from 'moment';
moment().format('YYYY-MM-DD');

// After: dayjs (2KB gzipped) — same API
import dayjs from 'dayjs';
dayjs().format('YYYY-MM-DD');
Enter fullscreen mode Exit fullscreen mode

Same API. 97% smaller. Check the full comparison at pkgpulse.com/compare/dayjs-vs-moment.

The Lodash Problem

Lodash is 71KB when you import _ from 'lodash'. Most projects use 5-10 functions. The fix:

// Bad: imports all of lodash (71KB)
import _ from 'lodash';
_.debounce(fn, 300);

// Better: cherry-pick
import debounce from 'lodash/debounce';

// Best: use lodash-es for tree shaking
import { debounce } from 'lodash-es';

// Best of all: do you even need it?
// Many lodash functions have native equivalents in 2026
const unique = [...new Set(array)];          // _.uniq
const grouped = Object.groupBy(items, fn);    // _.groupBy (ES2024)
const flat = array.flat(Infinity);            // _.flattenDeep
Enter fullscreen mode Exit fullscreen mode

Before reaching for a utility library, check if the native API covers your use case. In 2026, it usually does.

Step 3: Enable Tree Shaking

Tree shaking removes unused code from your bundle. It works automatically with ES modules, but there are common pitfalls:

Make Sure It's Actually Working

  1. Use ES module importsimport { x } enables tree shaking. require() does not.
  2. Check sideEffects — packages need "sideEffects": false in their package.json for optimal shaking
  3. Avoid barrel filesimport { Button } from './components' often imports everything in the barrel
  4. Use production mode — tree shaking only runs in production builds

A Common Mistake

// Breaks tree shaking — imports the entire barrel
import { Button } from '@/components';

// Direct import — only Button code is bundled
import { Button } from '@/components/Button';
Enter fullscreen mode Exit fullscreen mode

This one change can save tens of kilobytes in component-heavy applications.

Step 4: Code Split by Route

Don't load your entire app upfront. Split by route so users download only what they need:

// Next.js — automatic per-page splitting
// Each file in /app is a separate chunk

// React Router — lazy loading
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
Enter fullscreen mode Exit fullscreen mode

Lazy-Load Heavy Libraries

// Always loaded — 100KB hits every user on first load
import { Chart } from 'chart.js';

// Loaded on demand — only users who view charts pay the cost
const renderChart = async (data) => {
  const { Chart } = await import('chart.js');
  new Chart(canvas, { data });
};
Enter fullscreen mode Exit fullscreen mode

If a library is only used on one page or behind a user action, dynamic import() is almost always the right call.

Step 5: Optimize Your Build Tool

Your build tool directly affects output size. Compare options on PkgPulse:

  • Vite (Rollup under the hood) — better tree shaking, smaller output by default
  • Webpack 5 — more configurable, but requires more manual optimization
  • Turbopack — Webpack's successor, still maturing

Compression Matters

Always serve compressed assets. The savings are dramatic:

Compression Typical Savings
None 0% (baseline)
Gzip ~60-70% reduction
Brotli ~70-80% reduction

Brotli compresses JavaScript ~15-20% better than Gzip. Most CDNs and hosting platforms (Vercel, Cloudflare, Netlify) support it automatically — make sure it's enabled.

Step 6: Set Budgets and Automate

Optimization is meaningless without enforcement. Set up automated bundle budgets in CI:

{
  "size-limit": [
    { "path": "dist/**/*.js", "limit": "150 KB" },
    { "path": "dist/index.js", "limit": "50 KB" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Run npx size-limit in your CI pipeline. The build fails if the budget is exceeded. Tools like bundlesize can comment on PRs with the size impact of every change, creating accountability before code ships.

Real-World Example: 450KB to 120KB

Here's a realistic optimization journey for a mid-size React app:

Step Action Impact Running Total
Start Initial bundle 450KB
1 moment → dayjs -70KB 380KB
2 Tree-shake lodash -50KB 330KB
3 Lazy-load chart library -80KB 250KB
4 Tree-shake icon library (lucide) -40KB 210KB
5 Code-split by route -60KB 150KB
6 Enable Brotli compression -30KB 120KB

Result: 73% reduction. Load time dropped from 3.2 seconds to 1.1 seconds on a 4G connection. That's the difference between a user staying and a user bouncing.

The Takeaway

Bundle optimization isn't a one-time task — it's an ongoing practice. The highest-impact steps, in order:

  1. Measure first — you can't optimize what you can't see
  2. Swap heavy dependencies — use PkgPulse to compare before installing
  3. Code-split by route — users shouldn't pay for pages they don't visit
  4. Set automated budgets — prevent regression in CI

Every kilobyte you cut makes your app faster, your SEO stronger, and your users happier.

Compare package sizes on PkgPulse →


Frequently Asked Questions

What is a good JavaScript bundle size?

For most web apps, aim for under 200KB of JavaScript (gzipped) on the initial page load. Under 100KB is excellent. The critical factor is the per-route bundle, not the total app size — code splitting ensures users only download what they need. Use PkgPulse to compare dependency sizes before adding them.

How do I check my bundle size?

Use webpack-bundle-analyzer or source-map-explorer to visualize what's in your existing bundle. For pre-install comparisons, PkgPulse shows bundle size, health scores, and download trends for any npm package.

Does bundle size affect SEO?

Yes. Google's Core Web Vitals — LCP, INP, and CLS — are all affected by JavaScript bundle size. Large bundles delay rendering, block interactivity, and cause layout shifts. Sites that fail Core Web Vitals metrics receive lower search rankings.


Explore popular comparisons: Vite vs Webpack, Day.js vs Moment, Axios vs Ky on PkgPulse.

Top comments (0)