Every kilobyte of JavaScript is parsed, compiled, and executed before your user can interact with the page. A 2 MB bundle on a mid-range mobile device on a 4G connection takes 3–5 seconds to be interactive — and that is before your API calls start. Bundle size is not a vanity metric; it is directly correlated with bounce rate, conversion, and Core Web Vitals scores. The 2026 tooling makes finding and fixing the problem faster than ever.
What changed in 2026
- Vite 6 improved tree-shaking and code-splitting defaults — projects that migrated from Webpack to Vite often saw 20–40% bundle reductions with zero code changes.
- Module Federation v2 (Webpack / Rspack) made micro-frontend code sharing practical at scale, allowing teams to share vendor chunks across apps.
- React Server Components (stable in Next.js 15, Remix 3) moved component code to the server entirely — some client bundles shrank by 60%+ after migration.
bundlephobia and pkg-size matured into standard pre-commit tools for checking dependency cost before merging.
Anatomy of a typical bloated bundle
| Category |
Common culprits |
Typical size (minified + gzip) |
| UI framework |
React + ReactDOM |
~40 KB |
| Date library |
moment.js |
~70 KB (avoidable) |
| Charting library |
Chart.js full build |
~60 KB |
| Component library |
MUI or Ant Design (full import) |
~200–400 KB |
| Utility library |
lodash (full import) |
~70 KB |
| Icons |
FontAwesome full bundle |
~400 KB |
Step 1: audit first
# Vite
npx vite-bundle-visualizer
# Webpack
npm install --save-dev webpack-bundle-analyzer
# add to webpack config:
# plugins: [new BundleAnalyzerPlugin()]
Also use the Lighthouse bundle audit in Chrome DevTools — it flags unused JavaScript per file with byte counts.
Step 2: tree-shaking — named imports only
// BAD — imports the entire lodash library (~70 KB)
import _ from 'lodash';
const result = _.debounce(fn, 300);
// GOOD — imports only debounce (~2 KB)
import debounce from 'lodash/debounce';
// or use the ES-modules version:
import { debounce } from 'lodash-es';
Tree-shaking only works when the library ships ES modules (look for "module" or "exports" with import in package.json). CommonJS libraries cannot be tree-shaken.
Step 3: code-split by route
// React — lazy load routes
import { lazy, Suspense } from 'react';
const AdminDashboard = lazy(() => import('./pages/AdminDashboard'));
const ReportsPage = lazy(() => import('./pages/Reports'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/reports" element={<ReportsPage />} />
</Routes>
</Suspense>
);
}
A typical e-commerce app can split checkout, dashboard, and admin into separate chunks, reducing the initial bundle by 50–70%.
Step 4: replace heavy dependencies
| Heavy library |
Lightweight replacement |
Savings |
| moment.js (70 KB) |
date-fns (tree-shakeable, ~3–10 KB per function) |
~60 KB |
| Chart.js full |
Chart.js tree-shaken or Recharts |
~30–40 KB |
| lodash full |
lodash-es (tree-shakeable) |
~50–60 KB |
| FontAwesome full |
Only import used icons via SVG |
~350 KB |
| axios |
native fetch (zero KB) |
~15 KB |
Check the cost of any new dependency before adding it: npx bundlephobia <package-name>.
Step 5: analyse what is left
After the obvious wins, check for:
- Duplicate packages — two versions of React or a shared utility. Use
npm dedupe or check with npx duplicate-package-checker-webpack-plugin.
- Polyfills you do not need — if your target is modern browsers (> 2024), many polyfills are dead weight. Review your browserslist config.
- Source maps in production bundles — they should never be part of the JS bundle sent to browsers.
How to pick optimisation priorities
- Bundle > 500 KB (gzip)? Route-based code splitting first — this is almost always the fastest win.
- One library dominates the analysis chart? Replace or tree-shake it.
- Many medium-sized chunks? Check for duplicate vendor code across chunks; consolidate with
splitChunks.
- Images inside JS? Inline SVGs in JS are fine for small icons; base64-encoded images in JS are not — move them to the asset pipeline.
- Already code-split and tree-shaken? Look at preload strategies and prefetch for non-critical chunks.
Common mistakes
Importing component libraries without tree-shaking. import { Button } from '@mui/material' imports the entire Material UI tree in many configurations. Use the babel plugin or explicit path imports.
Not setting production mode. Vite and Webpack both produce development builds without NODE_ENV=production. Development builds can be 3× larger due to debug code.
Lazy loading everything. Lazily loading the hero section below the fold means it appears after a flash of unstyled content. Only lazy-load what users will not see immediately.
Ignoring the vendor chunk. A single massive vendor chunk with all dependencies defeats the caching benefits of code splitting. Split vendors by package stability.
Not measuring after changes. Always re-run the analyser after each change to confirm the expected saving was achieved.
What to skip
- Manual minification — your bundler handles this; do not add a separate step.
- Removing React DevTools check manually —
NODE_ENV=production handles this automatically.
- Compressing assets in JavaScript — use build-time tools or CDN transforms.
FAQ
What is a good target for initial bundle size?
Under 150 KB (gzip) for the critical path is a reasonable 2026 target for a content-heavy app. SPAs can go higher but should code-split aggressively. Core Web Vitals (INP, LCP) are the real success metric.
Does switching from Webpack to Vite automatically reduce bundle size?
Often yes, due to better tree-shaking defaults and native ES module support. But the larger wins come from fixing imports and splitting routes — the bundler matters less than what you put in it.
How do I measure if my bundle change improved performance?
Use Lighthouse or WebPageTest against a production build. Look at Total Blocking Time (TBT) and Time to Interactive (TTI), not just transferred bytes.
Should I use a CDN for libraries like React?
Shared CDN caches (jsDelivr, unpkg) lost their advantage when HTTP/2 and aggressive browser caching made self-hosting competitive. Self-host for better cache control and no third-party dependency risk.
Where to go next
See How to set up ESLint in 2026, Webpack vs Vite in 2026, and How to profile slow code in 2026.