Bundle Size Optimization: Tools and Techniques for 2026
Every kilobyte of JavaScript you ship costs your users time and your business money. A 100KB bundle takes 1-2 seconds to parse on a mid-range phone. For e-commerce, each extra second of load time reduces conversions by 7%.
Here's how to measure, analyze, and shrink your JavaScript bundles in 2026.
Why Bundle Size Matters
The Performance Tax
| Bundle Size | Parse Time (Mobile) | Parse Time (Desktop) |
|---|---|---|
| 100 KB | 200-300ms | 50-100ms |
| 500 KB | 1-1.5s | 200-400ms |
| 1 MB | 2-3s | 400-800ms |
| 2 MB | 4-6s | 800ms-1.5s |
This is just parse time — before the code even runs. Add execution time, and large bundles create a noticeably sluggish experience.
The SEO Impact
Google's Core Web Vitals directly factor in JavaScript performance:
- Largest Contentful Paint (LCP) — Heavy bundles delay content rendering
- Interaction to Next Paint (INP) — Large bundles block the main thread
- Time to First Byte (TTFB) — Larger transfers take longer
Step 1: Measure What You Have
Before optimizing, measure. You can't improve what you don't understand.
Bundle Analysis Tools
webpack-bundle-analyzer
The classic. Generates an interactive treemap of your bundle contents.
npm install --save-dev webpack-bundle-analyzer
# Add to webpack config
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [new BundleAnalyzerPlugin()]
};
source-map-explorer
Lighter weight than webpack-bundle-analyzer. Works with any build tool that generates source maps.
npx source-map-explorer dist/bundle.js
@next/bundle-analyzer
Purpose-built for Next.js projects:
npm install @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({ /* your config */ });
# Run
ANALYZE=true npm run build
bundlephobia.com
Check the size of any npm package before you install it. Shows minified size, gzipped size, download time, and composition.
Check package sizes on PkgPulse too — we show bundle size alongside health scores and download trends.
Step 2: Tree-Shaking
Tree-shaking removes unused code from your bundle. Modern bundlers (Vite, webpack 5, Rspack) do this automatically for ES modules.
Make Sure It Works
Tree-shaking only works with ES module syntax (import/export). CommonJS (require) can't be tree-shaken.
// ✅ Tree-shakeable — only 'debounce' is included in the bundle
import { debounce } from 'lodash-es';
// ❌ NOT tree-shakeable — entire lodash library is included
const { debounce } = require('lodash');
Choose Tree-Shakeable Packages
When comparing packages on PkgPulse, prefer those that ship ES modules. Look for "module" or "exports" fields in package.json.
Common swaps for smaller bundles:
| Heavy Package | Lighter Alternative | Size Reduction |
|---|---|---|
lodash (72KB) | lodash-es (tree-shakeable) | 60-90% |
moment (72KB) | date-fns (tree-shakeable) | 80-95% |
axios (13KB) | ky (3KB) or native fetch | 75-100% |
uuid (3.5KB) | crypto.randomUUID() (0KB) | 100% |
Step 3: Code Splitting
Don't load everything upfront. Split your bundle into chunks that load on demand.
Route-Based Splitting (React)
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Component-Based Splitting
Lazy-load heavy components that aren't immediately visible:
// Only load the chart library when the user scrolls to the chart section
const Chart = lazy(() => import('./components/Chart'));
function Analytics() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<h1>Analytics</h1>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<Chart />
</Suspense>
)}
</div>
);
}
Dynamic Imports for Heavy Libraries
// Don't import marked at the top level
// import { marked } from 'marked'; // ❌ 40KB added to main bundle
// Load it when needed
async function renderMarkdown(text) {
const { marked } = await import('marked'); // ✅ Loaded on demand
return marked(text);
}
Step 4: Replace Heavy Dependencies
The biggest wins often come from swapping out bloated packages.
Date Libraries
| Library | Size (min+gzip) | Tree-Shakeable |
|---|---|---|
| Moment.js | 72KB | ❌ (deprecated) |
| date-fns | 2-10KB (per function) | ✅ |
| Day.js | 2.9KB | Partial |
| Temporal (native) | 0KB | N/A (built-in) |
UI Component Libraries
| Library | Full Size | With Tree-Shaking |
|---|---|---|
| Ant Design | 340KB | ~80-150KB |
| Material UI | 300KB | ~60-120KB |
| Radix + Tailwind | ~20-40KB | ~15-30KB |
| shadcn/ui | ~15-30KB | ~10-25KB |
Compare component libraries on PkgPulse to make informed choices.
Step 5: Compression
Gzip vs Brotli
| Compression | Typical Ratio | Browser Support |
|---|---|---|
| None | 1x | All |
| Gzip | 3-5x | 99%+ |
| Brotli | 4-6x | 97%+ |
Brotli compresses 15-20% better than gzip. Most CDNs and hosting providers support it. Enable it:
// Next.js (next.config.js)
module.exports = {
compress: true, // Gzip by default
};
// Vite (vite.config.js)
import viteCompression from 'vite-plugin-compression';
export default {
plugins: [
viteCompression({ algorithm: 'brotliCompress' }),
],
};
Step 6: Set a Budget
Define a bundle size budget and enforce it in CI:
// bundlesize config in package.json
{
"bundlesize": [
{
"path": "./dist/main.*.js",
"maxSize": "150 kB"
},
{
"path": "./dist/vendor.*.js",
"maxSize": "250 kB"
}
]
}
Or use Lighthouse CI with performance budgets:
{
"ci": {
"assert": {
"assertions": {
"resource-summary:script:size": ["error", { "maxNumericValue": 300000 }]
}
}
}
}
Optimization Checklist
- ✅ Analyze your bundle with a visualization tool
- ✅ Ensure tree-shaking works (ES modules, sideEffects field)
- ✅ Code-split routes and heavy components
- ✅ Replace heavy dependencies with lighter alternatives
- ✅ Enable Brotli compression
- ✅ Set and enforce a bundle size budget
- ✅ Lazy-load below-the-fold content
- ✅ Use dynamic imports for rarely-used features
- ✅ Remove unused CSS (PurgeCSS or Tailwind's built-in purge)
- ✅ Optimize images (next/image, sharp, AVIF format)
Conclusion
Bundle size optimization isn't a one-time task — it's an ongoing discipline. Start by measuring, then make targeted improvements where the data shows the biggest impact. The tools and techniques in 2026 make it easier than ever to ship fast, lean JavaScript.
Use PkgPulse to compare package sizes before adding new dependencies — it's the easiest way to prevent bundle bloat before it starts.