Bundle Analyzer
Analyzes JavaScript bundle sizes, identifies optimization opportunities, and helps reduce bundle size for faster page loads.
When to Use
-
"Analyze my bundle size"
-
"Why is my bundle so large?"
-
"Optimize webpack bundle"
-
"Reduce bundle size"
-
"Find large dependencies"
-
"Setup bundle analysis"
Instructions
- Detect Build Tool
Check which bundler is being used:
Check package.json
grep -E "(webpack|vite|rollup|parcel|esbuild)" package.json
Check config files
[ -f "webpack.config.js" ] && echo "Webpack" [ -f "vite.config.js" ] && echo "Vite" [ -f "rollup.config.js" ] && echo "Rollup"
- Install Analysis Tool
For Webpack:
npm install --save-dev webpack-bundle-analyzer
For Vite:
npm install --save-dev rollup-plugin-visualizer
For Rollup:
npm install --save-dev rollup-plugin-visualizer
Cross-platform:
npm install --save-dev source-map-explorer
- Configure Analysis
Webpack
webpack.config.js:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = { // ... other config plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'static', reportFilename: 'bundle-report.html', openAnalyzer: true, generateStatsFile: true, statsFilename: 'bundle-stats.json' }) ] }
Or for conditional analysis:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = { plugins: [ process.env.ANALYZE && new BundleAnalyzerPlugin() ].filter(Boolean) }
package.json scripts:
{ "scripts": { "build": "webpack", "build:analyze": "ANALYZE=true webpack", "analyze": "webpack-bundle-analyzer dist/stats.json" } }
Vite
vite.config.js:
import { defineConfig } from 'vite' import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({ plugins: [ visualizer({ open: true, gzipSize: true, brotliSize: true, filename: 'dist/stats.html' }) ], build: { rollupOptions: { output: { manualChunks: { vendor: ['react', 'react-dom'], utils: ['lodash', 'date-fns'] } } } } })
Next.js
next.config.js:
const { ANALYZE } = process.env
module.exports = { webpack: (config, { isServer }) => { if (ANALYZE) { const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer') config.plugins.push( new BundleAnalyzerPlugin({ analyzerMode: 'static', reportFilename: isServer ? '../analyze/server.html' : './analyze/client.html' }) ) } return config } }
Or use @next/bundle-analyzer:
npm install --save-dev @next/bundle-analyzer
const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' })
module.exports = withBundleAnalyzer({ // Next.js config })
package.json:
{ "scripts": { "analyze": "ANALYZE=true next build" } }
Create React App
npm install --save-dev source-map-explorer
package.json:
{ "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'" } }
- Analyze Bundle
Run analysis:
npm run build:analyze
or
npm run analyze
Generate report showing:
-
Total bundle size
-
Breakdown by module
-
Treemap visualization
-
Gzipped sizes
-
Duplicate dependencies
- Identify Issues
Common issues to look for:
Large Dependencies
-
Moment.js (use date-fns or dayjs instead)
-
Lodash (use lodash-es or individual functions)
-
Full libraries when only using small parts
Duplicate Dependencies
-
Same package included multiple times
-
Different versions of same package
Unused Code
-
Dead code not tree-shaken
-
CSS/JS not actually used
Large Images/Assets
-
Images not optimized
-
SVGs not compressed
Development Code in Production
-
Console logs
-
Dev-only packages
-
Source maps in production
- Suggest Optimizations
Optimization Strategies
- Code Splitting
// Dynamic imports const HeavyComponent = lazy(() => import('./HeavyComponent'))
// Route-based splitting const Dashboard = lazy(() => import('./pages/Dashboard')) const Settings = lazy(() => import('./pages/Settings'))
// Webpack magic comments const module = import( /* webpackChunkName: "my-chunk" / / webpackPrefetch: true */ './module' )
- Tree Shaking
// ❌ BAD: Imports entire library import _ from 'lodash'
// ✅ GOOD: Import only what you need import debounce from 'lodash/debounce' import throttle from 'lodash/throttle'
// ✅ BETTER: Use lodash-es for tree-shaking import { debounce, throttle } from 'lodash-es'
- Replace Large Libraries
// ❌ BAD: Moment.js (heavy) import moment from 'moment'
// ✅ GOOD: date-fns (modular) import { format, parseISO } from 'date-fns'
// ✅ GOOD: dayjs (lightweight) import dayjs from 'dayjs'
- Lazy Load Routes (React Router)
import { lazy, Suspense } from 'react' import { BrowserRouter, Routes, Route } from 'react-router-dom'
const Home = lazy(() => import('./pages/Home')) const About = lazy(() => import('./pages/About')) const Dashboard = lazy(() => import('./pages/Dashboard'))
function App() { return ( <BrowserRouter> <Suspense fallback={<div>Loading...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/dashboard" element={<Dashboard />} /> </Routes> </Suspense> </BrowserRouter> ) }
- Manual Chunks (Vite/Rollup)
// vite.config.js export default { build: { rollupOptions: { output: { manualChunks(id) { // Vendor chunk for node_modules if (id.includes('node_modules')) { if (id.includes('react') || id.includes('react-dom')) { return 'react-vendor' } if (id.includes('@mui')) { return 'mui-vendor' } return 'vendor' } } } } } }
- Externalize Dependencies (CDN)
// webpack.config.js module.exports = { externals: { react: 'React', 'react-dom': 'ReactDOM', lodash: '_' } }
<!-- index.html --> <script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js"></script>
- Optimize Images
// next.config.js module.exports = { images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920], } }
// Use next/image import Image from 'next/image'
<Image src="/photo.jpg" width={500} height={300} alt="Photo" />
- Remove Unused CSS
Install PurgeCSS
npm install --save-dev @fullhuman/postcss-purgecss
// postcss.config.js module.exports = { plugins: [ require('@fullhuman/postcss-purgecss')({ content: ['./src/**/*.{js,jsx,ts,tsx}'], defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [] }) ] }
- Compression
// webpack.config.js const CompressionPlugin = require('compression-webpack-plugin')
module.exports = { plugins: [ new CompressionPlugin({ algorithm: 'gzip', test: /.(js|css|html|svg)$/, threshold: 10240, minRatio: 0.8 }) ] }
- Environment-Specific Code
// webpack.config.js const webpack = require('webpack')
module.exports = { plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }) ] }
// In code if (process.env.NODE_ENV !== 'production') { // This will be removed in production build console.log('Development mode') }
- Size Budgets
webpack.config.js:
module.exports = { performance: { maxEntrypointSize: 250000, // 250kb maxAssetSize: 250000, hints: 'warning' } }
package.json with size-limit:
npm install --save-dev size-limit @size-limit/preset-app
{ "size-limit": [ { "path": "dist/bundle.js", "limit": "300 KB" }, { "path": "dist/vendor.js", "limit": "200 KB" } ] }
- CI/CD Integration
GitHub Actions:
name: Bundle Size Check
on: [pull_request]
jobs: size: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - run: npm ci - run: npm run build
- name: Check bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
9. Monitoring
Set up continuous monitoring:
// Report to analytics if (typeof window !== 'undefined' && window.performance) { const perfData = window.performance.getEntriesByType('resource')
perfData.forEach(entry => {
if (entry.name.includes('.js')) {
console.log(${entry.name}: ${(entry.transferSize / 1024).toFixed(2)} KB)
}
})
}
Lighthouse CI:
.lighthouserc.js
module.exports = { ci: { assert: { assertions: { 'total-byte-weight': ['error', { maxNumericValue: 1000000 }], 'resource-summary:script:size': ['error', { maxNumericValue: 500000 }] } } } }
- Generate Report
Create comprehensive report:
Bundle Analysis Report
Summary
- Total bundle size: 450 KB (gzipped: 150 KB)
- Number of chunks: 5
- Largest chunk: vendor.js (200 KB)
Top 10 Largest Dependencies
- moment.js - 72 KB (❌ Consider replacing with date-fns)
- lodash - 65 KB (⚠️ Use lodash-es for tree-shaking)
- @mui/material - 120 KB (✅ Already code-split)
- react-dom - 40 KB (✅ Essential)
- chart.js - 35 KB (⚠️ Lazy load if not on first page)
Optimization Opportunities
- Replace moment.js with date-fns (-50 KB)
- Use lodash-es instead of lodash (-30 KB)
- Lazy load chart.js (-35 KB)
- Code split routes (-80 KB initial load)
- Remove unused CSS (-15 KB)
Estimated Savings
Total potential reduction: 210 KB (47% smaller) New bundle size: 240 KB
Action Items
- Immediate: Replace moment.js
- Short-term: Implement route-based code splitting
- Long-term: Audit and remove unused dependencies
Best Practices
DO:
-
Analyze on every major release
-
Set size budgets and enforce them
-
Use code splitting for routes
-
Lazy load below-the-fold content
-
Tree-shake effectively
-
Monitor bundle size in CI/CD
DON'T:
-
Import entire libraries
-
Ignore duplicate dependencies
-
Skip production optimizations
-
Forget to compress assets
-
Include dev dependencies in production
-
Use large polyfills unnecessarily
Size Targets
General Guidelines:
-
Main bundle: < 200 KB (gzipped)
-
Vendor bundle: < 300 KB (gzipped)
-
Total page weight: < 1 MB
-
Time to Interactive: < 3 seconds on 3G
Mobile-first:
-
First bundle: < 100 KB (gzipped)
-
Critical CSS: < 14 KB (first TCP round)
-
Above-fold images: < 200 KB
Analysis Checklist
-
Bundle analysis tool installed
-
Baseline measurements taken
-
Large dependencies identified
-
Duplicate dependencies found
-
Code splitting implemented
-
Tree shaking verified
-
Size budgets set
-
CI/CD checks added
-
Production build optimized
-
Monitoring in place