Code Splitting — Splitting SPA Bundles into Smaller Chunks
In this tutorial, you will learn about Code Splitting. We cover key concepts, practical examples, and best practices to help you master this topic.
Code splitting divides your SPA JavaScript bundle into smaller chunks loaded on demand, reducing initial download size and improving load performance for users on slow connections.
What You'll Learn
By the end of this tutorial, you will understand code splitting strategies (route-based, vendor, and component-level), how to configure Webpack and Vite for splitting, and how to analyze bundle output.
Why It Matters
A single 500KB JavaScript bundle blocks rendering for over 2 seconds on mobile 3G. Code splitting reduces this to under 1 second by loading only the code needed for the current page. For large SPAs, code splitting can cut initial load time by 60-80%.
Real-World Use
A SaaS dashboard SPA with 200+ components split their bundle into: a 50KB vendor chunk (React, React Router), a 30KB app shell, and 5-20KB page chunks. The initial login page loaded in 1.2 seconds instead of 4.5 seconds. Users on slow connections saw a 73% improvement.
Code Splitting Strategies
Code Splitting Strategies
┌──────────────────────────────────────────────────────────────┐
│ Strategy 1: Route-Based Splitting │
│ / → home.chunk.js (15KB) │
│ /dashboard → dashboard.chunk.js (25KB) │
│ /settings → settings.chunk.js (12KB) │
│ │
│ Strategy 2: Vendor Splitting │
│ vendor.chunk.js (React, Redux, Router) — 150KB │
│ app.chunk.js (your code) — 50KB │
│ │
│ Strategy 3: Component-Level Splitting │
│ Chart library → charts.chunk.js (80KB, lazy loaded) │
│ Markdown editor → editor.chunk.js (60KB, lazy loaded) │
└──────────────────────────────────────────────────────────────┘
Think of code splitting like packing for a trip. Instead of one giant suitcase (single bundle), you use multiple smaller bags: a carry-on for essentials (app shell), a checked bag for clothes (vendor), and specific bags for activities you might do (lazy-loaded features).
Webpack Code Splitting
// webpack.config.js — Route-based and vendor splitting
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].chunk.js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: 10
},
common: {
minChunks: 2,
minSize: 10000,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
Vite Code Splitting
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
// Vendor chunk
if (id.includes('node_modules')) {
if (id.includes('react')) return 'vendor-react';
if (id.includes('lodash')) return 'vendor-lodash';
return 'vendor';
}
// Route chunks (auto-handled by Vite for dynamic imports)
},
chunkFileNames: 'assets/[name]-[hash].js',
entryFileNames: 'assets/[name]-[hash].js'
}
}
}
});
Analyzing Bundle Output
// package.json scripts
{
"scripts": {
"analyze": "webpack-bundle-analyzer dist/stats.json",
"build:stats": "webpack --profile --json > dist/stats.json",
"analyze:vite": "vite build && npx vite-bundle-analyzer"
}
}
// Programmatic bundle analysis
import { statSync } from 'fs';
import { join } from 'path';
function analyzeBundles(distDir) {
const files = [
'vendor.[hash].js',
'main.[hash].js',
'dashboard.[hash].js',
'settings.[hash].js'
];
files.forEach(pattern => {
const file = findFile(distDir, pattern);
if (file) {
const stats = statSync(join(distDir, file));
const sizeKB = (stats.size / 1024).toFixed(1);
console.log(`${file}: ${sizeKB}KB`);
}
});
}
Dynamic Imports for Code Splitting
// Route-level dynamic imports
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Users = React.lazy(() => import('./pages/Users'));
// Vendor chunk extraction
// webpack automatically extracts shared modules into vendor chunks
// Vite does this by default
// Named exports from dynamic imports
async function loadFeature() {
const { exportFeature, helperFunction } = await import('./features/export');
exportFeature();
}
Common Mistakes
- Not extracting vendor chunks. Without vendor splitting, library code (React, Redux) is included in every route chunk or in one giant bundle. Extract vendors once.
- Splitting into too many chunks. Each chunk has HTTP overhead (headers, TLS negotiation). More than 10-15 chunks per page may hurt performance.
- Ignoring chunk name collisions. Without content hashes, cached browsers may serve stale chunks. Always use [contenthash] in filenames.
- Not analyzing bundle output. Guessing what is in your bundle leads to missed opportunities. Always analyze with webpack-bundle-analyzer or source-map-explorer.
- Splitting synchronous imports. Code splitting only works with dynamic import() syntax. Static imports are always bundled together.
Practice Questions
- What are the three main code splitting strategies?
- How does vendor splitting differ from route-based splitting?
- Why should you use content hashes in chunk filenames?
- How do you analyze your bundle to find large dependencies?
- What is the difference between static and dynamic imports for code splitting?
Challenge: Set up code splitting in an SPA with 3 strategies: vendor chunk (all node_modules), route-based chunks (6 pages), and component-level Lazy Loading (a chart library). Analyze the output with webpack-bundle-analyzer. Achieve initial bundle under 100KB.
FAQ
Mini Project
Set up code splitting in a Vite-based React SPA with 8 pages. Configure vendor chunk, 8 route-level chunks, and lazy-load a heavy chart library component. Analyze with vite-bundle-analyzer. Measure initial vs total bundle size. Document the optimization achieved.
What's Next
You split your bundles. Now master dynamic imports — the syntax that powers code splitting and lazy loading.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro