Why Bundle Size Matters More Than Your Framework Choice
·PkgPulse Team
TL;DR
The framework debate is mostly irrelevant to performance if you're shipping a 2MB bundle. React adds ~45KB. Svelte adds ~2KB. But the apps people are actually building have 300-500KB of third-party libraries, 200-400KB of application code, and sometimes 500KB+ from a single chart library they used on one route. Switching from React to Svelte would save ~43KB. Lazy-loading that chart component saves 500KB on first load. The math is obvious — yet developers spend weeks debating frameworks and minutes on bundle analysis.
Key Takeaways
- React is ~45KB — tiny relative to your application code and libraries
- A typical SaaS app ships 500KB-2MB of JavaScript to first-time users
- 100KB more JS = ~1 second slower on average mobile (3G, ARM processor)
- Code splitting + lazy loading has 10x more impact than framework choice
- The biggest wins: remove duplicate dependencies, lazy-load heavy routes
The Real Bundle Size Breakdown
What's actually in a typical React SaaS app bundle?
Before optimization (typical):
→ React + ReactDOM: 45KB
→ React Router: 24KB
→ TanStack Query: 13KB
→ date-fns (not tree-shaken): 81KB
→ Chart.js: 62KB (used on /dashboard route only)
→ framer-motion: 47KB (used for 3 animations)
→ Your application code: 250KB
→ Duplicate lodash copies: 70KB
→ i18n library + translations: 120KB (all locales)
→ Total: 712KB
After optimization:
→ React + ReactDOM: 45KB (can't avoid)
→ React Router: 24KB
→ TanStack Query: 13KB
→ date-fns (tree-shaken): 3KB (only functions used)
→ Chart.js: 0KB (lazy loaded on /dashboard)
→ framer-motion (basic): 20KB (tree-shaken to what's used)
→ Your application code: 200KB (code split by route)
→ Duplicate lodash: 0KB (deduped)
→ i18n: 30KB (only current locale, lazy rest)
→ Total first load: 335KB
Savings: ~377KB on first load = ~3 seconds on mobile 3G
Framework switch React → Svelte would have saved: 43KB
Code splitting saved: 377KB
The math is obvious.
Why This Matters for Real Users
The average user isn't your developer laptop on fiber:
Device stats (global internet traffic, 2025):
→ 60% of web traffic: mobile devices
→ Average mobile processor: 4-8x slower than a MacBook
→ Average mobile connection: LTE (fast) to 3G (slow)
→ Median global download speed: 12Mbps (vs US median 50Mbps)
JavaScript cost:
1. Download: obvious (bandwidth)
2. Parse: convert text → AST (CPU intensive)
3. Compile: JIT compile to machine code (CPU intensive)
4. Execute: run the JavaScript (CPU intensive)
The parse + compile + execute cost is often ignored.
On an average Android device:
→ Downloading 1MB of JS: ~1 second (LTE)
→ Parsing + compiling 1MB of JS: ~5 seconds (low-end CPU)
So a 1MB bundle costs ~6 seconds before your app does anything.
Real user data (from web performance research):
→ 53% of mobile users abandon a page that takes >3 seconds to load
→ 100ms delay = 1% conversion rate decrease
→ 1 second delay = 7% conversion rate decrease
You're losing real customers to that chart library you imported globally.
The Bundle Analysis Workflow
# 1. Measure your current bundle (do this first, always)
# Next.js:
npm install --save-dev @next/bundle-analyzer
# next.config.ts:
import bundleAnalyzer from '@next/bundle-analyzer';
const withBundleAnalyzer = bundleAnalyzer({ enabled: process.env.ANALYZE === 'true' });
export default withBundleAnalyzer({});
# Run: ANALYZE=true npm run build
# Opens interactive treemap of your bundle
# Vite:
npm install --save-dev rollup-plugin-visualizer
# vite.config.ts:
import { visualizer } from 'rollup-plugin-visualizer';
// Add to plugins: visualizer({ open: true })
# Run: npm run build
# Opens stats.html
# 2. Look for the big blocks in the visualization
# Common offenders:
# → date libraries (moment: 300KB, date-fns: 81KB before tree-shaking)
# → chart libraries (echarts: 900KB!, Chart.js: 62KB, Recharts: 52KB)
# → rich text editors (CKEditor: 500KB+, Slate: 100KB+)
# → i18n library with ALL locales included
# → Duplicate packages (react appears twice in different versions)
# → Development-only code in production bundle
# 3. Find which routes use which code
npm run build 2>&1 | grep "First Load JS" # Next.js
# Typical output:
# ┌ ○ / 54.3 kB
# ├ ○ /dashboard 412.7 kB ← This is your problem
# ├ ○ /settings 48.1 kB
# └ ○ /profile 52.3 kB
# /dashboard is 8x larger because of Chart.js being loaded eagerly
The Fixes with the Highest ROI
// Fix 1: Dynamic imports for heavy components
// BEFORE: chart loads on every page
import { BarChart } from './BarChart';
// AFTER: chart only loads when user navigates to dashboard
const BarChart = dynamic(() => import('./BarChart'), {
loading: () => <Skeleton />, // Next.js
});
// Or in Vite:
const BarChart = lazy(() => import('./BarChart'));
// Saves: whatever BarChart's dependencies cost (often 50-200KB)
// Fix 2: Tree-shake date libraries
// BEFORE (moment - not tree-shakeable):
import moment from 'moment';
const formatted = moment().format('MMM D, YYYY');
// Costs: 300KB
// AFTER (date-fns - tree-shakeable):
import { format } from 'date-fns';
const formatted = format(new Date(), 'MMM d, yyyy');
// Costs: ~3KB (just the format function)
// Fix 3: Load translations lazily
// BEFORE: all locales upfront
import i18n from 'i18next';
import enTranslations from './locales/en.json';
import esTranslations from './locales/es.json';
import frTranslations from './locales/fr.json';
// Costs: all locale files on first load
// AFTER: only current locale
i18n.init({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
});
// Fetches only the user's locale, lazily
// Fix 4: Split by route automatically
// Next.js does this automatically per page
// Vite with React Router:
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
// Each route's code only loads when that route is visited
// Fix 5: Find duplicate packages
npx npm-dedupe # Or: pnpm dedupe
# Duplicate packages happen when two deps need different versions
# Often: two versions of lodash, two versions of react-is
# Cost: doubled bundle size for affected packages
The Framework Size Question (For Context)
Since we're being quantitative:
Framework gzipped sizes (just the framework):
Svelte: ~2KB runtime
Solid: ~7KB
Preact: ~4KB
Vue 3: ~33KB
React: ~45KB
Angular: ~150KB
Reality check:
→ Your app code: 200-500KB
→ Your third-party libraries: 50-300KB
→ Framework: 2-150KB
Switching from React to Svelte:
→ Saves 43KB
→ That's less than one average third-party library
→ Less than one poorly-optimized route's lazy loading savings
This doesn't mean framework size is irrelevant:
→ Angular's 150KB is real and matters
→ Svelte's 2KB is genuinely impressive
→ For small sites and edge cases, these numbers matter
But the marginal difference between React (45KB) and Svelte (2KB)
is 43KB — the equivalent of one Axios import.
For most apps, this is not the bottleneck.
The frameworks debate is worth having.
Just don't confuse it with performance engineering.
They're different conversations.
Check bundle sizes for any npm package at PkgPulse.
See the live comparison
View date fns vs. dayjs on PkgPulse →