$ -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