Bundle Size Reduction: Cut Your JavaScript by 40% With These Techniques
Large JavaScript bundles kill performance. Learn tree-shaking, code-splitting, and modern bundler tricks to dramatically reduce what you ship to users.
Why Bundle Size Matters More Than Ever
Every kilobyte of JavaScript must be downloaded, parsed, and compiled before it executes. On mid-range mobile devices on 4G connections — which represent the majority of global web traffic — 1MB of JS can add 3-5 seconds to Time to Interactive.
The good news: most apps have 30-50% dead code that never runs. Here's how to find and eliminate it.
Start With Bundle Analysis
Before optimizing, know what you're shipping:
# For Vite projects
npm install --save-dev rollup-plugin-visualizer
# vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({ open: true, gzipSize: true })
]
};
# For Next.js
npm install @next/bundle-analyzer
# next.config.js — wrap your config with withBundleAnalyzer
Look for duplicate dependencies (lodash AND lodash-es), unnecessarily large libraries, and modules you thought were tree-shaken but aren't.
Tree-Shaking: Import Only What You Use
Tree-shaking works on ES modules. The classic offender is lodash:
// BAD: imports entire lodash (72kb gzipped)
import _ from 'lodash';
const result = _.groupBy(items, 'category');
// GOOD: imports only groupBy (~2kb)
import groupBy from 'lodash-es/groupBy';
const result = groupBy(items, 'category');
// BETTER: use native where possible
const result = Object.groupBy(items, item => item.category);
The same applies to date libraries. Replace moment.js (67kb) with date-fns and import only the functions you need.
Code Splitting at Route Level
Never ship all your routes to the initial page load:
// React Router with lazy loading
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
);
}
Dynamic Imports for Heavy Components
Some components (rich text editors, chart libraries, PDF viewers) are heavy but only needed on interaction:
import { useState } from 'react';
function DocumentEditor() {
const [Editor, setEditor] = useState(null);
async function loadEditor() {
const { default: TipTap } = await import('./TipTapEditor');
setEditor(() => TipTap);
}
if (!Editor) {
return <button onClick={loadEditor}>Open Editor</button>;
}
return <Editor />;
}
Replace Heavy Libraries
Many common libraries have lightweight alternatives:
moment.js(67kb) →date-fnstree-shakeable orTemporalAPIaxios(14kb) →fetchwith a thin wrapperlodash(72kb) → native ES2023+ methodsclassnames→ template literalsuuid→crypto.randomUUID()
Configure Modern Bundler Optimizations
// vite.config.ts
export default {
build: {
// Use modern output for modern browsers
target: 'es2022',
// Split vendor chunks intelligently
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('react')) return 'react-vendor';
if (id.includes('@radix-ui')) return 'radix-vendor';
return 'vendor';
}
}
}
}
}
};
Results to Expect
Following these techniques on a typical React app:
- Tree-shaking lodash/date libraries: -40 to -70kb
- Route-level code splitting: -50 to -200kb initial load
- Dynamic heavy components: -30 to -100kb initial load
- Library swaps: -20 to -60kb
Combine them all and a 500kb bundle can become 150kb — a genuine 3x improvement in load time on slow connections.
Admin
Cal.com
Open source scheduling — tự host booking system, thay thế Calendly. Free & privacy-first.
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!