PatternsCode Splitting Strategies
Library Splitting (Vendor Chunks)
Separate third-party libraries for better caching and performance
Library Splitting (Vendor Chunks)
Split third-party libraries (vendor code) from your application code for better caching and optimal bundle sizes.
🎯 Why Split Vendor Code?
Without Vendor Splitting:
main.js: 2.5 MB (app + vendors)
└─ Changes frequently
└─ Users redownload everything on every deploy
With Vendor Splitting:
main.js: 300 KB (app code only)
├─ Changes frequently
vendor.js: 500 KB (React, etc)
├─ Changes rarely → cached!
charts.js: 400 KB (Chart.js)
└─ Changes never → cached forever!
Result: 80% less redownloads on updates! ✅📦 Webpack Configuration
Basic Vendor Split
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// React vendor chunk
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'vendor',
priority: 10
},
// Other node_modules
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'commons',
priority: 5
}
}
}
}
};Advanced Split (Recommended)
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
maxInitialRequests: 25, // Allow more chunks
minSize: 20000, // Min 20 KB per chunk
cacheGroups: {
// React core (rarely changes)
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'vendor-react',
priority: 40
},
// UI libraries
ui: {
test: /[\\/]node_modules[\\/](@radix-ui|@headlessui)[\\/]/,
name: 'vendor-ui',
priority: 35
},
// Charts (heavy!)
charts: {
test: /[\\/]node_modules[\\/](chart\.js|react-chartjs-2)[\\/]/,
name: 'vendor-charts',
priority: 30
},
// Date utilities
dates: {
test: /[\\/]node_modules[\\/](date-fns|dayjs|moment)[\\/]/,
name: 'vendor-dates',
priority: 25
},
// Utils
utils: {
test: /[\\/]node_modules[\\/](lodash|ramda|underscore)[\\/]/,
name: 'vendor-utils',
priority: 20
},
// Remaining vendors
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
}
}
}
}
};⚡ Vite Configuration
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
// React vendor
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
// UI libraries
'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
// Heavy libraries
'vendor-charts': ['chart.js', 'react-chartjs-2'],
'vendor-editor': ['slate', 'slate-react'],
// Utilities
'vendor-utils': ['lodash-es', 'date-fns'],
// Icons
'vendor-icons': ['lucide-react', '@heroicons/react']
}
}
},
// Chunk size warnings
chunkSizeWarningLimit: 500, // Warn if chunk > 500 KB
}
};🎯 Strategy: Split by Update Frequency
// Group by how often they change
manualChunks: {
// NEVER changes (cache forever)
'vendor-stable': [
'react',
'react-dom'
],
// RARELY changes (cache 1 year)
'vendor-libs': [
'lodash-es',
'date-fns',
'axios'
],
// SOMETIMES changes (cache 1 month)
'vendor-ui': [
'@radix-ui/react-dialog',
'@headlessui/react'
],
// OFTEN changes (cache 1 week)
'vendor-internal': [
'@company/design-system',
'@company/utils'
]
}📊 Analyzing Bundle Size
Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static', // Generates HTML file
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
]
};npm run build
# Opens bundle-report.html showing treemapVite Bundle Visualizer
npm install --save-dev rollup-plugin-visualizer// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [
visualizer({
open: true,
gzipSize: true,
brotliSize: true,
})
]
};🎨 Dynamic Vendor Splitting
// Split vendors dynamically based on size
export default {
build: {
rollupOptions: {
output: {
manualChunks(id) {
// Node modules
if (id.includes('node_modules')) {
// Large libraries get their own chunk
if (id.includes('chart.js')) {
return 'vendor-charts';
}
if (id.includes('monaco-editor')) {
return 'vendor-editor';
}
if (id.includes('@tensorflow')) {
return 'vendor-ml';
}
// Group small vendors together
return 'vendor';
}
}
}
}
}
};⚙️ Next.js Optimization
Next.js has good defaults, but you can optimize further:
// next.config.js
module.exports = {
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
// React/Next.js framework
framework: {
test: /[\\/]node_modules[\\/](react|react-dom|next)[\\/]/,
name: 'framework',
priority: 40
},
// Heavy libraries
charts: {
test: /[\\/]node_modules[\\/](chart\.js)[\\/]/,
name: 'charts',
priority: 30
},
// Default vendors
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 20
}
}
};
}
return config;
}
};🔍 Finding Heavy Dependencies
# Install dependency analysis tool
npm install -g cost-of-modules
# Run analysis
cost-of-modules
# Output:
# chart.js: 421 KB
# moment: 289 KB ← Replace with date-fns!
# lodash: 71 KB ← Use lodash-es + tree-shaking!Replace Heavy Dependencies
// ❌ BAD: Moment.js (289 KB!)
import moment from 'moment';
moment().format('YYYY-MM-DD');
// ✅ GOOD: date-fns (13 KB!)
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');
// ❌ BAD: Lodash (71 KB!)
import _ from 'lodash';
_.map(array, fn);
// ✅ GOOD: Lodash-es (tree-shakeable!)
import { map } from 'lodash-es';
map(array, fn);🎯 Chunk Loading Strategy
// Prioritize chunk loading
export default {
build: {
rollupOptions: {
output: {
// Name chunks by priority
chunkFileNames: (chunkInfo) => {
if (chunkInfo.name === 'vendor-react') {
return 'assets/critical-[name]-[hash].js';
}
if (chunkInfo.name.startsWith('vendor-')) {
return 'assets/vendor-[name]-[hash].js';
}
return 'assets/[name]-[hash].js';
}
}
}
}
};Preload Critical Vendors
<!-- index.html -->
<head>
<!-- Preload critical vendor chunks -->
<link rel="preload" href="/assets/critical-vendor-react.js" as="script">
<link rel="preload" href="/assets/vendor-ui.js" as="script">
<!-- Prefetch less critical -->
<link rel="prefetch" href="/assets/vendor-charts.js" as="script">
</head>📚 Best Practices
1. Target Chunk Sizes
Ideal chunk sizes:
- Main bundle: < 200 KB (gzipped)
- Vendor chunks: < 150 KB each (gzipped)
- Route chunks: < 100 KB each (gzipped)
If vendor > 150 KB → Split further!2. Cache Strategy
Set cache headers:
- vendor-react.js: Cache 1 year (rarely changes)
- vendor-libs.js: Cache 6 months
- main.js: Cache 1 day (changes often)3. Monitor Over Time
# Add to package.json
"scripts": {
"build:analyze": "npm run build && webpack-bundle-analyzer stats.json"
}
# Run weekly
npm run build:analyze
# Check for:
# - Chunks > 500 KB (too big!)
# - Duplicate dependencies
# - Unused code🏢 Real-World Examples
Shopify
// Shopify splits:
// - React core (stable)
// - Polaris UI (updates monthly)
// - Charts (lazy loaded)
// - App code (updates daily)Stripe
// Stripe splits:
// - React/framework (cached forever)
// - Stripe.js (rarely changes)
// - Dashboard code (changes often)
// - Analytics libs (lazy loaded)📚 Key Takeaways
- Split by update frequency - Cache what doesn't change
- Target < 150 KB per chunk - Gzipped size
- Analyze your bundle - Use bundle analyzer
- Replace heavy deps - Moment → date-fns, Lodash → Lodash-es
- Preload critical chunks - React, UI libs
- Prefetch heavy chunks - Charts, editors
- Monitor over time - Bundle size grows quickly
Vendor splitting typically saves 60-70% bandwidth on updates. Users only redownload your app code! 🚀