Code Splitting: Optimize Bundle Size with Dynamic Imports
Code splitting is essential for large applications. Learn when and how to split your code for optimal performance.
Quick Navigation: Route-Based Code Splitting • Component-Based Splitting • Dynamic Imports • Vendor Splitting • Best Practices
Quick Decision Guide
Quick Implementation Guide:
Route-based: Use React.lazy() with Suspense for page-level splitting. Each route becomes a separate chunk.
Component-based: Split heavy components (charts, editors, modals) that aren't immediately visible. Use dynamic imports: const Chart = lazy(() => import('./Chart'))
Vendor splitting: Configure bundler (Webpack/Vite) to separate node_modules automatically. Vendor code changes less frequently, improving cache hits.
Dynamic imports: Load modules conditionally: if (user.isPremium) await import('./PremiumFeature')
Best Practice: Start with route-based splitting, then component-based for heavy features. Monitor bundle sizes - split if initial bundle >100KB.
Route-Based Code Splitting
React Router Example
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load routes
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>
);
}How It Works
1. User visits "/" → Only Home chunk loads
2. User navigates to "/about" → About chunk loads on-demand
3. Each route is a separate bundle
Bundle Structure
main.js (100KB) → Core app code
home.chunk.js (50KB) → Home page
about.chunk.js (60KB) → About page
dashboard.chunk.js (80KB) → Dashboard page
vendor.js (200KB) → Third-party librariesInitial load: main.js + home.chunk.js = 150KB (instead of 490KB)
Component-Based Splitting
Lazy Load Heavy Components
import { lazy, Suspense, useState } from 'react';
// Heavy component loaded on-demand
const Chart = lazy(() => import('./Chart'));
const DataTable = lazy(() => import('./DataTable'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>
Show Chart
</button>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
)}
</div>
);
}When to Use
Example: Modal Component
const Modal = lazy(() => import('./Modal'));
function App() {
const [showModal, setShowModal] = useState(false);
return (
<>
<button onClick={() => setShowModal(true)}>Open Modal</button>
{showModal && (
<Suspense fallback={null}>
<Modal onClose={() => setShowModal(false)} />
</Suspense>
)}
</>
);
}Dynamic Imports
Native Dynamic Import
// Static import (bundled)
import utils from './utils';
// Dynamic import (code split)
const loadUtils = async () => {
const utils = await import('./utils');
return utils.default;
};
// Usage
const utils = await loadUtils();
utils.doSomething();With Error Handling
async function loadFeature(featureName) {
try {
const module = await import('./features/' + featureName);
return module.default;
} catch (error) {
console.error(`Failed to load ${featureName}:`, error);
// Fallback
return null;
}
}
// Usage
const feature = await loadFeature('analytics');
if (feature) {
feature.init();
}Conditional Loading
// Load based on user permissions
if (user.isAdmin) {
const AdminPanel = await import('./AdminPanel');
render(<AdminPanel />);
}
// Load based on feature flags
if (featureFlags.newDashboard) {
const NewDashboard = await import('./NewDashboard');
return <NewDashboard />;
}Vendor Splitting
Separate Vendor Code
Vendor code (node_modules) changes less frequently than app code.
Separating it improves caching.
Webpack Configuration
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};Result
Before:
bundle.js (500KB) - Everything together
After:
vendors.js (300KB) - Third-party code (rarely changes)
app.js (200KB) - Your code (changes frequently)Benefit: When you update app code, users only re-download app.js, not vendors.js
Best Practices
When to Code Split
Do Split
Don't Split
Optimization Tips
1. Preload Critical Chunks
<link rel="preload" href="/chunks/critical.js" as="script">2. Prefetch Future Routes
// Prefetch on hover
<Link
to="/dashboard"
onMouseEnter={() => import('./pages/Dashboard')}
>
Dashboard
</Link>3. Loading States
<Suspense fallback={<LoadingSpinner />}>
<LazyComponent />
</Suspense>4. Error Boundaries
<ErrorBoundary fallback={<ErrorPage />}>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>Measuring Impact
// Measure chunk load time
const startTime = performance.now();
await import('./HeavyComponent');
const loadTime = performance.now() - startTime;
console.log(`Chunk loaded in ${loadTime}ms`);