Code splitting is one of the most powerful optimization techniques for modern web applications. By splitting your code into smaller chunks, you can load only what's needed, when it's needed, dramatically improving initial load times.
Understanding Code Splitting
Code splitting divides your bundle into smaller pieces that can be loaded on demand. This reduces the initial bundle size and improves Time to Interactive (TTI).
Basic Dynamic Imports
// Basic dynamic import
async function loadModule() {
const module = await import('./heavy-module.js');
module.doSomething();
}
// Conditional loading
if (process.env.NODE_ENV === 'development') {
const devTools = await import('./dev-tools');
devTools.init();
}React Code Splitting
Route-Based Splitting
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));
const Profile = lazy(() => import('./pages/Profile'));
const Admin = lazy(() => import('./pages/Admin'));
// Loading component
function PageLoader() {
return (
<div className="flex items-center justify-center min-h-screen">
<Spinner size="large" />
</div>
);
}
// Error boundary for lazy loading
class LazyErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
return this.props.children;
}
}
function App() {
return (
<BrowserRouter>
<LazyErrorBoundary>
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/cart" element={<Cart />} />
<Route path="/checkout" element={<Checkout />} />
<Route path="/profile" element={<Profile />} />
<Route path="/admin" element={<Admin />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Suspense>
</LazyErrorBoundary>
</BrowserRouter>
);
}Component-Based Splitting
import { lazy, Suspense, useState } from 'react';
// Lazy load heavy components
const RichTextEditor = lazy(() => import('./RichTextEditor'));
const DataGrid = lazy(() => import('./DataGrid'));
const Chart = lazy(() => import('./Chart'));
const Map = lazy(() => import('./Map'));
function Dashboard() {
const [showEditor, setShowEditor] = useState(false);
const [showChart, setShowChart] = useState(false);
return (
<div className="dashboard">
<button onClick={() => setShowEditor(true)}>
Create Post
</button>
{showEditor && (
<Suspense fallback={<Spinner />}>
<RichTextEditor />
</Suspense>
)}
<button onClick={() => setShowChart(true)}>
View Analytics
</button>
{showChart && (
<Suspense fallback={<Spinner />}>
<Chart data={analyticsData} />
</Suspense>
)}
</div>
);
}Named Exports with Code Splitting
// ✅ Use default export for easier lazy loading
export default function MyComponent() {
return <div>...</div>;
}
// Usage
const Component = lazy(() => import('./MyComponent'));
// ❌ Named exports require intermediate re-export
export function ComponentA() { return <div>A</div>; }
export function ComponentB() { return <div>B</div>; }
// Re-export as default in component.js
export { ComponentA as default } from './ComponentA';
// Usage
const ComponentA = lazy(() => import('./components/ComponentA'));Vue.js Code Splitting
Route-Based Splitting
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true }
}
];
const router = createRouter({
history: createWebHistory(),
routes
});Component Lazy Loading
<template>
<div>
<button @click="showChart = true">Show Chart</button>
<Suspense>
<template #default>
<Chart v-if="showChart" :data="chartData" />
</template>
<template #fallback>
<div>Loading chart...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { defineAsyncComponent, ref } from 'vue';
const showChart = ref(false);
const Chart = defineAsyncComponent(() =>
import('@/components/Chart.vue')
);
</script>Async Component with Error Handling
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200, // Show loading after 200ms
timeout: 10000 // Error after 10s
});Advanced Splitting Patterns
Prefetching and Preloading
import { lazy } from 'react';
// Prefetch: Load during browser idle time
const Dashboard = lazy(() =>
import(/* webpackPrefetch: true */ './Dashboard')
);
// Preload: Load in parallel to parent bundle
const CriticalComponent = lazy(() =>
import(/* webpackPreload: true */ './CriticalComponent')
);
// Webpack magic comments for better naming
const About = lazy(() =>
import(/* webpackChunkName: "about-page" */ './About')
);
// Multiple entries in same chunk
const UserSettings = lazy(() =>
import(/* webpackChunkName: "settings" */ './UserSettings')
);
const AccountSettings = lazy(() =>
import(/* webpackChunkName: "settings" */ './AccountSettings')
);Splitting by Features
// Feature-based splitting
const authFeature = () => import('./features/auth');
const checkoutFeature = () => import('./features/checkout');
const adminFeature = () => import('./features/admin');
// Load entire feature when needed
async function initializeAuth() {
const { login, logout, isAuthenticated } = await authFeature();
return { login, logout, isAuthenticated };
}
async function openCheckout() {
const { Cart, Payment, Shipping } = await checkoutFeature();
return { Cart, Payment, Shipping };
}Vendor Splitting
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// React vendor chunk
reactVendor: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'react-vendor',
priority: 10,
},
// UI library chunk
uiVendor: {
test: /[\\/]node_modules[\\/](@mui|@material-ui|framer-motion)[\\/]/,
name: 'ui-vendor',
priority: 9,
},
// Utility libraries
utilsVendor: {
test: /[\\/]node_modules[\\/](lodash|axios|dayjs)[\\/]/,
name: 'utils-vendor',
priority: 8,
},
// Common code
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
},
},
},
},
};Webpack Code Splitting
Entry Point Splitting
module.exports = {
entry: {
main: './src/main.js',
admin: './src/admin.js',
vendor: ['react', 'react-dom', 'lodash'],
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 0,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `npm.${packageName.replace('@', '')}`;
},
},
},
},
},
};Dynamic Import Syntax
// Basic dynamic import
import('./modules/myModule').then(module => {
module.doSomething();
});
// With error handling
import('./modules/myModule')
.then(module => {
module.default();
})
.catch(error => {
console.error('Failed to load module:', error);
// Load fallback
return import('./modules/fallback');
});
// Async/await syntax
async function loadComponent() {
try {
const { default: Component } = await import('./Component');
return Component;
} catch (error) {
console.error('Load failed:', error);
return ErrorComponent;
}
}Vite Code Splitting
Manual Chunk Splitting
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
// React ecosystem
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
// UI libraries
'ui-vendor': ['@mui/material', '@emotion/react', '@emotion/styled'],
// Utilities
'utils': ['lodash-es', 'axios', 'dayjs'],
// Larger libraries
'editor': ['@monaco-editor/react'],
'charts': ['recharts', 'd3-scale'],
},
},
},
},
};Performance Optimization
Progressive Loading
import { lazy, Suspense } from 'react';
// Load critical content first
const Hero = lazy(() => import(/* webpackPrefetch: true */ './Hero'));
// Load below-fold content later
const Features = lazy(() => import('./Features'));
const Testimonials = lazy(() => import('./Testimonials'));
const Pricing = lazy(() => import('./Pricing'));
function LandingPage() {
return (
<div>
<Suspense fallback={<HeroSkeleton />}>
<Hero />
</Suspense>
<Suspense fallback={<ContentSkeleton />}>
<Features />
<Testimonials />
<Pricing />
</Suspense>
</div>
);
}Intersection Observer Splitting
import { lazy, Suspense, useEffect, useState, useRef } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function LazyOnScroll() {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, []);
return (
<div ref={ref}>
{isVisible ? (
<Suspense fallback={<Spinner />}>
<HeavyComponent />
</Suspense>
) : (
<div style={{ height: '400px' }} />
)}
</div>
);
}Monitoring and Analysis
Bundle Analysis
// Track loaded chunks
const chunkTracker = new Set();
window.addEventListener('load', () => {
performance.getEntriesByType('resource').forEach(resource => {
if (resource.name.includes('.js')) {
const chunkName = resource.name.split('/').pop();
chunkTracker.add(chunkName);
console.log(`Chunk loaded: ${chunkName}`);
}
});
console.log(`Total chunks loaded: ${chunkTracker.size}`);
});Best Practices
- 1Split by Route: Route-based splitting provides the biggest impact
- 2Lazy Load Heavy Components: Load charts, editors, and maps on demand
- 3Prefetch Intelligently: Prefetch likely next pages during idle time
- 4Vendor Splitting: Separate frequently updated libraries from stable ones
- 5Set Loading States: Always provide visual feedback during loading
- 6Error Boundaries: Handle lazy loading failures gracefully
- 7Measure Impact: Use tools to measure actual performance improvements
- 8Avoid Over-Splitting: Too many chunks cause HTTP request overhead
Conclusion
Code splitting is essential for modern web performance. Start with route-based splitting, then progressively lazy-load heavy components and features. Monitor your bundle sizes and loading patterns to continuously optimize your splitting strategy.