Why Bundle Size Still Matters in 2026
TL;DR
Every 100KB of JavaScript costs ~1 second of interaction delay on median mobile hardware. 5G didn't solve the problem because the bottleneck shifted from download to parse and execute time. A 500KB bundle downloads in 250ms on 4G but takes 3-5 seconds to parse on a 2022 mid-range Android phone. Google's Core Web Vitals (specifically INP — Interaction to Next Paint) make this measurable and SEO-relevant. Bundle size is not a micro-optimization — it's the primary lever on perceived performance.
Key Takeaways
- 100KB gzipped ≈ 300KB uncompressed — gzip saves bandwidth; the browser still parses 300KB
- INP (Interaction to Next Paint) — replaced FID in Core Web Vitals; directly tied to JS size
- Median mobile phone — ~4x slower JS execution than a MacBook Pro
- 1s INP improvement → 7% conversion increase — Google's data from e-commerce analysis
- Tree-shaking + code splitting — the two levers that matter most
The Network Fallacy
"Connections are faster now, so bundle size doesn't matter as much."
This is wrong. Here's why:
2026 network speeds (global median):
Download: ~50 Mbps (10x faster than 2015)
Latency: ~20ms (similar to 2015)
What changed:
- Download time for a 500KB bundle: ~0.08s (fast!)
- Parse + compile time on Android mid-range: ~3.2s (same as 2015)
The bottleneck shifted from network to CPU.
Fast networks mean we download more JavaScript.
CPU performance has not kept pace with JS bundle growth.
JavaScript shipped per page (httparchive.org data):
2018: ~380KB median (gzipped)
2021: ~450KB median
2023: ~500KB median
2026: ~580KB median
The amount of JavaScript is growing faster than CPU performance.
The Real Cost of JavaScript
Cost analysis: 500KB gzipped JavaScript bundle
On a high-end MacBook (M3 Pro):
├── Download: 0.08s
├── Decompress: 0.02s
├── Parse: 0.15s
├── Compile (JIT): 0.08s
└── Execute: 0.12s
Total: ~0.45s ✅
On a median Android phone (2022 mid-range):
├── Download: 0.25s (slower network)
├── Decompress: 0.08s
├── Parse: 0.65s (4x slower CPU)
├── Compile (JIT): 0.45s
└── Execute: 0.50s
Total: ~1.93s ❌ (INP impact)
This is why "it's fast on my machine" doesn't mean "it's fast for your users."
Bundle Size by Package (What You're Actually Installing)
Selected package gzipped bundle contributions:
Framework/UI:
React + ReactDOM: ~45KB
Lodash (full): ~70KB ← Don't install full lodash
Lodash (cherry-pick): ~2KB ← Import specific functions
moment.js: ~72KB ← Use date-fns (~3KB) or Luxon (~18KB)
date-fns (full): ~75KB ← Use named imports for tree-shaking
date-fns (tree-shook): ~8KB ← import { format } from 'date-fns'
State Management:
Redux Toolkit: ~12KB
Zustand: ~1KB ← 12x smaller than Redux
Jotai: ~3KB
GraphQL Clients:
Apollo Client: ~47KB
urql: ~14KB ← 3x smaller
graphql-request: ~5KB ← 9x smaller
HTTP:
axios: ~14KB
ky: ~4KB
native fetch: 0KB ← Use this
Measuring Your Bundle
# Vite bundle analysis
npm install --save-dev rollup-plugin-visualizer
# vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
open: true, // Opens in browser after build
gzipSize: true, // Shows gzipped sizes
brotliSize: true,
}),
],
});
# Run: vite build
# Opens: dist/stats.html — interactive treemap of your bundle
# bundlephobia — check before installing
# https://bundlephobia.com/package/lodash@4.17.21
# Output: 71.5 kB gzip | 329 downloads/week per KB
# npmjs.com bundle size badge
# Most package pages show bundle size in the sidebar
# @next/bundle-analyzer — for Next.js
npm install @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: true });
module.exports = withBundleAnalyzer({});
# Run: ANALYZE=true next build
The Two Levers: Tree-Shaking and Code Splitting
1. Tree-Shaking (Eliminate Dead Code)
// ❌ Imports entire lodash — 70KB
import _ from 'lodash';
const result = _.groupBy(data, 'category');
// ✅ Cherry-pick: ~2KB
import groupBy from 'lodash/groupBy';
// ✅ Named import (if package supports tree-shaking):
import { groupBy } from 'lodash-es'; // ESM lodash
// ❌ date-fns bad import
import dateFns from 'date-fns';
// ✅ date-fns tree-shaken (3KB vs 75KB)
import { format, parseISO, differenceInDays } from 'date-fns';
// Check if a package is tree-shakeable:
// 1. Does it have "sideEffects": false in package.json?
// 2. Does it have an ESM build?
// 3. Does bundlephobia show a smaller size with named imports?
// Example: Material UI
import Button from '@mui/material/Button'; // ✅ Tree-shaken: ~15KB
import { Button } from '@mui/material'; // ✅ Also fine with MUI's proper exports
import * as MUI from '@mui/material'; // ❌ Entire library: ~300KB
2. Code Splitting (Load Later)
// Next.js — dynamic imports (code splitting)
import dynamic from 'next/dynamic';
// This component is split into a separate chunk
// Only loaded when user visits /admin
const AdminDashboard = dynamic(() => import('./AdminDashboard'), {
loading: () => <Skeleton />,
ssr: false, // Don't SSR admin components
});
// Vite — automatic code splitting with dynamic import
// Any dynamic import() creates a separate chunk
const chart = await import('./ChartComponent');
// Route-based splitting (React Router)
const routes = [
{
path: '/dashboard',
lazy: () => import('./pages/Dashboard'), // Loaded on demand
},
];
Core Web Vitals and Bundle Size
INP (Interaction to Next Paint) — the metric that measures JS impact:
Good: < 200ms
Needs improvement: 200-500ms
Poor: > 500ms
How bundle size affects INP:
1. Large initial bundle → long parse time → delayed interactivity
2. Synchronous scripts → blocking main thread → slow first interaction
3. Large event handlers → slow response to clicks/taps
Google's data:
- Sites in "Good" INP (< 200ms) have ~50% higher conversion rates
- 100ms INP improvement → ~2% revenue increase for e-commerce
Quick Wins Checklist
# Immediate wins (1-2 hours each):
□ Replace moment.js with date-fns or Luxon
Savings: ~65KB (gzipped)
□ Replace lodash with targeted imports or radash
Savings: ~40-60KB depending on usage
□ Remove unused icon packs (use only icons you need)
Savings: ~20-100KB (icon libraries are huge)
□ Enable dynamic imports for modals/dialogs
Savings: those components + their deps, loaded on demand
□ Use native fetch instead of axios
Savings: ~14KB
□ Switch from Apollo Client to urql or graphql-request
Savings: ~30-42KB
□ Run bundle analyzer to find unexpected large deps
Often finds: polyfills you don't need, duplicate packages, forgotten test utils
Compare package sizes on PkgPulse — bundle size data included for all comparisons.
See the live comparison
View vite vs. webpack on PkgPulse →