Tools: JavaScript Bundle Size Optimization: From 2MB to 200KB — A Practical Guide (2026)

Tools: JavaScript Bundle Size Optimization: From 2MB to 200KB — A Practical Guide (2026)

JavaScript Bundle Size Optimization: From 2MB to 200KB

Step 1: Analyze Before You Optimize

Webpack Bundle Analyzer

Vite Bundle Analysis

Bundlephobia for Quick Checks

Step 2: Replace Heavy Libraries

Moment.js → date-fns or Temporal API

Lodash → Native JavaScript

Axios → Fetch API

Step 3: Tree Shaking — Make It Actually Work

Common Tree Shaking Failures

Webpack Tree Shaking Config

Step 4: Code Splitting

Route-Based Splitting (React)

Dynamic Imports for Features

Webpack SplitChunksPlugin

Step 5: Compression

Brotli Compression (Best)

Vite/Webpack Compression Plugin

Step 6: Optimize Images and Assets in JS

Step 7: Measure the Real-World Impact

Core Web Vitals Targets

Bundle Size Budget

Quick Wins Checklist

Related Tools

Summary

Level Up Your Dev Workflow A 2MB JavaScript bundle is a performance emergency. On a 4G connection it takes 2-3 seconds to download and parse — before your app renders anything. Here's a systematic approach to cutting bundle size dramatically. Never guess. Measure first. Run npm run build and open bundle-report.html. You'll see a treemap of every dependency's contribution to your bundle. Common offenders: moment.js (330KB), lodash (70KB), date-fns (34KB per locale). Before adding any npm package, check bundlephobia.com for the bundle cost. Example: lodash costs 70KB — lodash-es with tree shaking costs 0-70KB depending on what you import. If you genuinely need lodash, import individual functions: Tree shaking removes unused code, but only works with ES modules (import/export syntax). Add "sideEffects": false to your package.json to tell bundlers your code is side-effect free: Don't ship code users haven't requested yet. Modern compression is dramatically effective on JavaScript. A 500KB JavaScript file typically compresses to: Serve pre-compressed .br files for maximum efficiency. Sometimes the "JavaScript bundle" includes base64-encoded assets. Bundle size and runtime performance aren't the same thing. Measure both. Bundle size primarily affects LCP (Largest Contentful Paint) and INP (Interaction to Next Paint). Enforce limits in CI to prevent regressions: Bundle size optimization follows a consistent pattern: measure → identify biggest offenders → replace or split → compress → enforce budgets. The highest-impact steps are replacing moment.js, adding route-based code splitting, and enabling Brotli compression. Together, these typically achieve 60-80% bundle size reduction before touching any application code. Start with the bundle analyzer. The treemap will immediately reveal which dependencies are worth replacing — it's almost always a few large libraries that can be swapped for smaller alternatives or modern native APIs. Found this useful? Explore DevPlaybook — cheat sheets, tool comparisons, and hands-on guides for modern developers. 🛒 Get the DevToolkit Starter Kit on Gumroad — 40+ browser-based dev tools, source code + deployment guide included. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 500;">npm -weight: 500;">install --save-dev webpack-bundle-analyzer # webpack.config.js const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); module.exports = { plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, reportFilename: 'bundle-report.html', }), ], }; -weight: 500;">npm -weight: 500;">install --save-dev webpack-bundle-analyzer # webpack.config.js const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); module.exports = { plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, reportFilename: 'bundle-report.html', }), ], }; -weight: 500;">npm -weight: 500;">install --save-dev webpack-bundle-analyzer # webpack.config.js const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); module.exports = { plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, reportFilename: 'bundle-report.html', }), ], }; -weight: 500;">npm -weight: 500;">install --save-dev rollup-plugin-visualizer # vite.config.ts import { visualizer } from 'rollup-plugin-visualizer'; export default { plugins: [ visualizer({ open: true, filename: 'dist/stats.html', gzipSize: true, brotliSize: true, }), ], }; -weight: 500;">npm -weight: 500;">install --save-dev rollup-plugin-visualizer # vite.config.ts import { visualizer } from 'rollup-plugin-visualizer'; export default { plugins: [ visualizer({ open: true, filename: 'dist/stats.html', gzipSize: true, brotliSize: true, }), ], }; -weight: 500;">npm -weight: 500;">install --save-dev rollup-plugin-visualizer # vite.config.ts import { visualizer } from 'rollup-plugin-visualizer'; export default { plugins: [ visualizer({ open: true, filename: 'dist/stats.html', gzipSize: true, brotliSize: true, }), ], }; // Before: moment.js (330KB) import moment from 'moment'; const formatted = moment(date).format('MMMM Do YYYY'); // After: date-fns (tree-shakeable, ~1KB for this function) import { format } from 'date-fns'; const formatted = format(date, 'MMMM do yyyy'); // Even better: Temporal API (zero bundle cost, native 2026) const formatted = new Temporal.PlainDate .from(date) .toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); // Before: moment.js (330KB) import moment from 'moment'; const formatted = moment(date).format('MMMM Do YYYY'); // After: date-fns (tree-shakeable, ~1KB for this function) import { format } from 'date-fns'; const formatted = format(date, 'MMMM do yyyy'); // Even better: Temporal API (zero bundle cost, native 2026) const formatted = new Temporal.PlainDate .from(date) .toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); // Before: moment.js (330KB) import moment from 'moment'; const formatted = moment(date).format('MMMM Do YYYY'); // After: date-fns (tree-shakeable, ~1KB for this function) import { format } from 'date-fns'; const formatted = format(date, 'MMMM do yyyy'); // Even better: Temporal API (zero bundle cost, native 2026) const formatted = new Temporal.PlainDate .from(date) .toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); // Before: lodash (70KB) import _ from 'lodash'; const result = _.groupBy(users, 'department'); const unique = _.uniqBy(items, 'id'); const sorted = _.sortBy(data, ['name', 'age']); // After: native JS (0KB) const result = Object.groupBy(users, u => u.department); // ES2024 const unique = [...new Map(items.map(i => [i.id, i])).values()]; const sorted = data.toSorted((a, b) => a.name.localeCompare(b.name) || a.age - b.age ); // Before: lodash (70KB) import _ from 'lodash'; const result = _.groupBy(users, 'department'); const unique = _.uniqBy(items, 'id'); const sorted = _.sortBy(data, ['name', 'age']); // After: native JS (0KB) const result = Object.groupBy(users, u => u.department); // ES2024 const unique = [...new Map(items.map(i => [i.id, i])).values()]; const sorted = data.toSorted((a, b) => a.name.localeCompare(b.name) || a.age - b.age ); // Before: lodash (70KB) import _ from 'lodash'; const result = _.groupBy(users, 'department'); const unique = _.uniqBy(items, 'id'); const sorted = _.sortBy(data, ['name', 'age']); // After: native JS (0KB) const result = Object.groupBy(users, u => u.department); // ES2024 const unique = [...new Map(items.map(i => [i.id, i])).values()]; const sorted = data.toSorted((a, b) => a.name.localeCompare(b.name) || a.age - b.age ); // Partial import with tree shaking import groupBy from 'lodash/groupBy'; import uniqBy from 'lodash/uniqBy'; // Partial import with tree shaking import groupBy from 'lodash/groupBy'; import uniqBy from 'lodash/uniqBy'; // Partial import with tree shaking import groupBy from 'lodash/groupBy'; import uniqBy from 'lodash/uniqBy'; // Before: axios (14KB) import axios from 'axios'; const { data } = await axios.get('/api/users'); // After: fetch (0KB, native) const data = await fetch('/api/users').then(r => r.json()); // With error handling async function fetchUsers() { const res = await fetch('/api/users'); if (!res.ok) throw new Error(`HTTP error: ${res.-weight: 500;">status}`); return res.json(); } // Before: axios (14KB) import axios from 'axios'; const { data } = await axios.get('/api/users'); // After: fetch (0KB, native) const data = await fetch('/api/users').then(r => r.json()); // With error handling async function fetchUsers() { const res = await fetch('/api/users'); if (!res.ok) throw new Error(`HTTP error: ${res.-weight: 500;">status}`); return res.json(); } // Before: axios (14KB) import axios from 'axios'; const { data } = await axios.get('/api/users'); // After: fetch (0KB, native) const data = await fetch('/api/users').then(r => r.json()); // With error handling async function fetchUsers() { const res = await fetch('/api/users'); if (!res.ok) throw new Error(`HTTP error: ${res.-weight: 500;">status}`); return res.json(); } // ❌ CommonJS: can't tree shake const { pick } = require('lodash'); // ✅ ESM: fully tree-shakeable import { pick } from 'lodash-es'; // ❌ Re-exporting entire library defeats tree shaking export * from 'some-library'; // ✅ Named re-exports preserve tree shaking export { Button, Input, Modal } from 'some-library'; // ❌ CommonJS: can't tree shake const { pick } = require('lodash'); // ✅ ESM: fully tree-shakeable import { pick } from 'lodash-es'; // ❌ Re-exporting entire library defeats tree shaking export * from 'some-library'; // ✅ Named re-exports preserve tree shaking export { Button, Input, Modal } from 'some-library'; // ❌ CommonJS: can't tree shake const { pick } = require('lodash'); // ✅ ESM: fully tree-shakeable import { pick } from 'lodash-es'; // ❌ Re-exporting entire library defeats tree shaking export * from 'some-library'; // ✅ Named re-exports preserve tree shaking export { Button, Input, Modal } from 'some-library'; // webpack.config.js module.exports = { mode: 'production', // Enables tree shaking automatically optimization: { usedExports: true, // Mark unused exports sideEffects: false, // Trust package.json sideEffects field }, }; // webpack.config.js module.exports = { mode: 'production', // Enables tree shaking automatically optimization: { usedExports: true, // Mark unused exports sideEffects: false, // Trust package.json sideEffects field }, }; // webpack.config.js module.exports = { mode: 'production', // Enables tree shaking automatically optimization: { usedExports: true, // Mark unused exports sideEffects: false, // Trust package.json sideEffects field }, }; { "sideEffects": ["*.css", "*.scss"] } { "sideEffects": ["*.css", "*.scss"] } { "sideEffects": ["*.css", "*.scss"] } // Before: all routes in one bundle import Dashboard from './Dashboard'; import Settings from './Settings'; import AdminPanel from './AdminPanel'; // After: each route loaded on demand const Dashboard = lazy(() => import('./Dashboard')); const Settings = lazy(() => import('./Settings')); const AdminPanel = lazy(() => import('./AdminPanel')); function App() { return ( <Suspense fallback={<Loading />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/admin" element={<AdminPanel />} /> </Routes> </Suspense> ); } // Before: all routes in one bundle import Dashboard from './Dashboard'; import Settings from './Settings'; import AdminPanel from './AdminPanel'; // After: each route loaded on demand const Dashboard = lazy(() => import('./Dashboard')); const Settings = lazy(() => import('./Settings')); const AdminPanel = lazy(() => import('./AdminPanel')); function App() { return ( <Suspense fallback={<Loading />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/admin" element={<AdminPanel />} /> </Routes> </Suspense> ); } // Before: all routes in one bundle import Dashboard from './Dashboard'; import Settings from './Settings'; import AdminPanel from './AdminPanel'; // After: each route loaded on demand const Dashboard = lazy(() => import('./Dashboard')); const Settings = lazy(() => import('./Settings')); const AdminPanel = lazy(() => import('./AdminPanel')); function App() { return ( <Suspense fallback={<Loading />}> <Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/admin" element={<AdminPanel />} /> </Routes> </Suspense> ); } // Load heavy features only when needed async function handleExport() { // xlsx is 200KB — only load when user clicks Export const { default: XLSX } = await import('xlsx'); const workbook = XLSX.utils.book_new(); // ... } // Load syntax highlighting only on code pages async function highlightCode(code, language) { const { highlight } = await import('highlight.js'); return highlight(code, { language }).value; } // Load heavy features only when needed async function handleExport() { // xlsx is 200KB — only load when user clicks Export const { default: XLSX } = await import('xlsx'); const workbook = XLSX.utils.book_new(); // ... } // Load syntax highlighting only on code pages async function highlightCode(code, language) { const { highlight } = await import('highlight.js'); return highlight(code, { language }).value; } // Load heavy features only when needed async function handleExport() { // xlsx is 200KB — only load when user clicks Export const { default: XLSX } = await import('xlsx'); const workbook = XLSX.utils.book_new(); // ... } // Load syntax highlighting only on code pages async function highlightCode(code, language) { const { highlight } = await import('highlight.js'); return highlight(code, { language }).value; } optimization: { splitChunks: { chunks: 'all', cacheGroups: { // Separate vendor chunks for better caching react: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, name: 'react', priority: 20, }, vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10, reuseExistingChunk: true, }, }, }, }, optimization: { splitChunks: { chunks: 'all', cacheGroups: { // Separate vendor chunks for better caching react: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, name: 'react', priority: 20, }, vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10, reuseExistingChunk: true, }, }, }, }, optimization: { splitChunks: { chunks: 'all', cacheGroups: { // Separate vendor chunks for better caching react: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, name: 'react', priority: 20, }, vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10, reuseExistingChunk: true, }, }, }, }, # nginx.conf gzip on; gzip_static on; brotli on; brotli_static on; brotli_comp_level 6; brotli_types text/javascript application/javascript application/json; # nginx.conf gzip on; gzip_static on; brotli on; brotli_static on; brotli_comp_level 6; brotli_types text/javascript application/javascript application/json; # nginx.conf gzip on; gzip_static on; brotli on; brotli_static on; brotli_comp_level 6; brotli_types text/javascript application/javascript application/json; // vite.config.ts import viteCompression from 'vite-plugin-compression'; export default { plugins: [ viteCompression({ algorithm: 'brotliCompress' }), viteCompression({ algorithm: 'gzip' }), ], }; // vite.config.ts import viteCompression from 'vite-plugin-compression'; export default { plugins: [ viteCompression({ algorithm: 'brotliCompress' }), viteCompression({ algorithm: 'gzip' }), ], }; // vite.config.ts import viteCompression from 'vite-plugin-compression'; export default { plugins: [ viteCompression({ algorithm: 'brotliCompress' }), viteCompression({ algorithm: 'gzip' }), ], }; // webpack.config.js — prevent base64 inlining for large assets module.exports = { module: { rules: [ { test: /\.(png|jpg|gif|svg)$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 4 * 1024, // Only inline files under 4KB }, }, }, ], }, }; // webpack.config.js — prevent base64 inlining for large assets module.exports = { module: { rules: [ { test: /\.(png|jpg|gif|svg)$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 4 * 1024, // Only inline files under 4KB }, }, }, ], }, }; // webpack.config.js — prevent base64 inlining for large assets module.exports = { module: { rules: [ { test: /\.(png|jpg|gif|svg)$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 4 * 1024, // Only inline files under 4KB }, }, }, ], }, }; // Performance Observer API: measure parsing time const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name.includes('chunk')) { console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`); } } }); observer.observe({ type: 'resource', buffered: true }); // Performance Observer API: measure parsing time const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name.includes('chunk')) { console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`); } } }); observer.observe({ type: 'resource', buffered: true }); // Performance Observer API: measure parsing time const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name.includes('chunk')) { console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`); } } }); observer.observe({ type: 'resource', buffered: true }); // package.json { "bundlesize": [ { "path": "./dist/js/main.*.js", "maxSize": "100 kB" }, { "path": "./dist/js/vendors.*.js", "maxSize": "200 kB" } ] } // package.json { "bundlesize": [ { "path": "./dist/js/main.*.js", "maxSize": "100 kB" }, { "path": "./dist/js/vendors.*.js", "maxSize": "200 kB" } ] } // package.json { "bundlesize": [ { "path": "./dist/js/main.*.js", "maxSize": "100 kB" }, { "path": "./dist/js/vendors.*.js", "maxSize": "200 kB" } ] } # GitHub Actions - name: Check bundle size run: npx bundlesize # GitHub Actions - name: Check bundle size run: npx bundlesize # GitHub Actions - name: Check bundle size run: npx bundlesize - Gzip: ~150KB (70% reduction) - Brotli: ~120KB (76% reduction) - [ ] Remove unused dependencies (npx depcheck) - [ ] Replace moment.js with date-fns - [ ] Replace lodash with native JS or lodash-es - [ ] Enable production mode in webpack/Vite - [ ] Add code splitting for routes - [ ] Enable Brotli/gzip compression on the server - [ ] Audit with webpack-bundle-analyzer - [ ] Set bundle size budgets in CI - TypeScript Performance Optimization 2026 — TypeScript-specific techniques - Web Vitals Optimization Guide — measure and improve Core Web Vitals - React Performance: useMemo vs useCallback vs memo — React-specific optimization - DevPlaybook Performance Tools — bundle analyzers, profilers, and benchmarks