Welcome to Code Splitting!
Imagine a library where you had to carry every book home just to read one. That would be exhausting! Code splitting is like having a librarian who brings you only the books you need, when you need them. Instead of loading your entire application at once, we load pieces on demand, making your app faster and more efficient.
graph TD
A[Initial Page Load] --> B{Code Splitting}
B --> C[Main Bundle
Essential Code] B --> D[Lazy Loaded
On Demand] C --> E[Fast Initial Load] D --> F[Dashboard Bundle] D --> G[Profile Bundle] D --> H[Admin Bundle] F --> I[Load When Needed] G --> I H --> I style C fill:#99ff99 style E fill:#99ff99 style D fill:#ffcc99 style I fill:#9999ff
Essential Code] B --> D[Lazy Loaded
On Demand] C --> E[Fast Initial Load] D --> F[Dashboard Bundle] D --> G[Profile Bundle] D --> H[Admin Bundle] F --> I[Load When Needed] G --> I H --> I style C fill:#99ff99 style E fill:#99ff99 style D fill:#ffcc99 style I fill:#9999ff
Understanding Code Splitting
1. Basic Lazy Loading with React.lazy
// Before: Everything loads at once
import ExpensiveComponent from './ExpensiveComponent';
import Dashboard from './Dashboard';
import Analytics from './Analytics';
import AdminPanel from './AdminPanel';
function App() {
return (
<div>
<ExpensiveComponent />
<Dashboard />
<Analytics />
<AdminPanel />
</div>
);
}
// After: Components load on demand
import React, { Suspense } from 'react';
// Lazy load components
const ExpensiveComponent = React.lazy(() => import('./ExpensiveComponent'));
const Dashboard = React.lazy(() => import('./Dashboard'));
const Analytics = React.lazy(() => import('./Analytics'));
const AdminPanel = React.lazy(() => import('./AdminPanel'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<ExpensiveComponent />
</Suspense>
<Suspense fallback={<div>Loading Dashboard...</div>}>
<Dashboard />
</Suspense>
<Suspense fallback={<div>Loading Analytics...</div>}>
<Analytics />
</Suspense>
<Suspense fallback={<div>Loading Admin Panel...</div>}>
<AdminPanel />
</Suspense>
</div>
);
}
2. Route-Based Code Splitting
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// Lazy load route components
const Home = React.lazy(() => import('./pages/Home'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Profile = React.lazy(() => import('./pages/Profile'));
const Settings = React.lazy(() => import('./pages/Settings'));
const AdminPanel = React.lazy(() => import('./pages/AdminPanel'));
// Loading component
function LoadingFallback() {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>Loading...</p>
</div>
);
}
function App() {
return (
<Router>
<div className="app">
<Navigation /> {/* This loads immediately */}
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
</div>
</Router>
);
}
// Advanced: Nested route splitting
const DashboardLayout = React.lazy(() => import('./layouts/DashboardLayout'));
const DashboardHome = React.lazy(() => import('./pages/Dashboard/Home'));
const DashboardAnalytics = React.lazy(() => import('./pages/Dashboard/Analytics'));
const DashboardReports = React.lazy(() => import('./pages/Dashboard/Reports'));
function DashboardRoutes() {
return (
<Suspense fallback={<LoadingFallback />}>
<Routes>
<Route element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<DashboardAnalytics />} />
<Route path="reports" element={<DashboardReports />} />
</Route>
</Routes>
</Suspense>
);
}
3. Component-Based Code Splitting
// Splitting individual components
const HeavyChart = React.lazy(() => import('./components/HeavyChart'));
const ComplexForm = React.lazy(() => import('./components/ComplexForm'));
const RichTextEditor = React.lazy(() => import('./components/RichTextEditor'));
function Dashboard() {
const [showChart, setShowChart] = useState(false);
const [showForm, setShowForm] = useState(false);
const [showEditor, setShowEditor] = useState(false);
return (
<div className="dashboard">
<h1>Dashboard</h1>
<div className="dashboard-controls">
<button onClick={() => setShowChart(true)}>
Show Analytics Chart
</button>
<button onClick={() => setShowForm(true)}>
Open Form
</button>
<button onClick={() => setShowEditor(true)}>
Open Editor
</button>
</div>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart onClose={() => setShowChart(false)} />
</Suspense>
)}
{showForm && (
<Suspense fallback={<div>Loading form...</div>}>
<ComplexForm onClose={() => setShowForm(false)} />
</Suspense>
)}
{showEditor && (
<Suspense fallback={<div>Loading editor...</div>}>
<RichTextEditor onClose={() => setShowEditor(false)} />
</Suspense>
)}
</div>
);
}
Advanced Code Splitting Patterns
1. Named Exports and Dynamic Imports
// utils.js
export function formatDate(date) {
return new Date(date).toLocaleDateString();
}
export function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}
export function calculateTax(amount, rate) {
return amount * rate;
}
// Using dynamic imports for named exports
async function processOrder(order) {
// Load only what we need
const { formatCurrency, calculateTax } = await import('./utils');
const tax = calculateTax(order.subtotal, 0.08);
const total = order.subtotal + tax;
return {
...order,
tax: formatCurrency(tax),
total: formatCurrency(total)
};
}
// Loading specific modules conditionally
function PaymentProcessor({ paymentMethod }) {
const [processor, setProcessor] = useState(null);
useEffect(() => {
async function loadProcessor() {
let module;
switch (paymentMethod) {
case 'stripe':
module = await import('./processors/stripe');
break;
case 'paypal':
module = await import('./processors/paypal');
break;
case 'square':
module = await import('./processors/square');
break;
default:
module = await import('./processors/default');
}
setProcessor(() => module.default);
}
loadProcessor();
}, [paymentMethod]);
if (!processor) {
return <div>Loading payment processor...</div>;
}
const ProcessorComponent = processor;
return <ProcessorComponent />;
}
2. Preloading Components
// Preload components for better UX
const AdminPanel = React.lazy(() => import('./AdminPanel'));
// Preload the component
const preloadAdminPanel = () => {
import('./AdminPanel');
};
function Navigation({ user }) {
// Preload admin panel when hovering over the link
const handleAdminHover = () => {
if (user.isAdmin) {
preloadAdminPanel();
}
};
return (
<nav>
<Link to="/">Home</Link>
<Link to="/dashboard">Dashboard</Link>
{user.isAdmin && (
<Link
to="/admin"
onMouseEnter={handleAdminHover}
onFocus={handleAdminHover}
>
Admin Panel
</Link>
)}
</nav>
);
}
// Advanced preloading with intersection observer
function PreloadOnScroll() {
const [Component, setComponent] = useState(null);
const triggerRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
// Start loading when element comes into view
import('./HeavyComponent').then(module => {
setComponent(() => module.default);
});
observer.disconnect();
}
},
{ threshold: 0.1 }
);
if (triggerRef.current) {
observer.observe(triggerRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div>
<div style={{ height: '100vh' }}>
Scroll down to load component...
</div>
<div ref={triggerRef}>
{Component ? (
<Component />
) : (
<div>Component will load when visible...</div>
)}
</div>
</div>
);
}
3. Error Boundaries with Lazy Components
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Component loading failed:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Failed to load component</h2>
<p>{this.state.error.message}</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// Wrapper for lazy components with retry logic
function LazyBoundary({ children }) {
return (
<ErrorBoundary>
<Suspense fallback={<LoadingSpinner />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// Advanced error handling with retry
function RetryableLazy({ importFn, maxRetries = 3 }) {
const [Component, setComponent] = useState(null);
const [error, setError] = useState(null);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
let mounted = true;
async function loadComponent() {
try {
const module = await importFn();
if (mounted) {
setComponent(() => module.default);
setError(null);
}
} catch (err) {
if (mounted) {
setError(err);
if (retryCount < maxRetries) {
// Retry with exponential backoff
setTimeout(() => {
setRetryCount(prev => prev + 1);
}, Math.pow(2, retryCount) * 1000);
}
}
}
}
loadComponent();
return () => {
mounted = false;
};
}, [importFn, retryCount, maxRetries]);
if (error && retryCount >= maxRetries) {
return (
<div className="error-message">
Failed to load component after {maxRetries} attempts.
<button onClick={() => setRetryCount(0)}>Try Again</button>
</div>
);
}
if (!Component) {
return <LoadingSpinner />;
}
return <Component />;
}
Performance Optimization Patterns
1. Bundle Analysis and Optimization
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// ... other config
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
],
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
// Split large libraries into separate chunks
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react',
chunks: 'all',
priority: 20,
},
lodash: {
test: /[\\/]node_modules[\\/]lodash[\\/]/,
name: 'lodash',
chunks: 'all',
priority: 20,
},
},
},
},
};
// Component-level optimization
// Dynamic imports with webpack magic comments
const HeavyComponent = React.lazy(() => import(
/* webpackChunkName: "heavy-component" */
/* webpackPreload: true */
'./HeavyComponent'
));
const AdminDashboard = React.lazy(() => import(
/* webpackChunkName: "admin" */
/* webpackPrefetch: true */
'./AdminDashboard'
));
2. Progressive Loading
// Progressive component loading
function ProgressiveImage({ src, alt, thumbnail }) {
const [currentSrc, setCurrentSrc] = useState(thumbnail);
const [loading, setLoading] = useState(true);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => {
setCurrentSrc(src);
setLoading(false);
};
}, [src]);
return (
<div className={`progressive-image ${loading ? 'loading' : ''}`}>
<img
src={currentSrc}
alt={alt}
style={{
filter: loading ? 'blur(10px)' : 'none',
transition: 'filter 0.3s ease'
}}
/>
</div>
);
}
// Progressive feature loading
function ProgressiveFeatures() {
const [features, setFeatures] = useState({
basic: true,
advanced: false,
premium: false
});
useEffect(() => {
// Load advanced features after basic UI
const advancedTimer = setTimeout(() => {
setFeatures(prev => ({ ...prev, advanced: true }));
}, 1000);
// Load premium features after advanced
const premiumTimer = setTimeout(() => {
setFeatures(prev => ({ ...prev, premium: true }));
}, 3000);
return () => {
clearTimeout(advancedTimer);
clearTimeout(premiumTimer);
};
}, []);
return (
<div>
<BasicFeatures />
{features.advanced && (
<Suspense fallback={<div>Loading advanced features...</div>}>
<AdvancedFeatures />
</Suspense>
)}
{features.premium && (
<Suspense fallback={<div>Loading premium features...</div>}>
<PremiumFeatures />
</Suspense>
)}
</div>
);
}
3. Resource Hints and Preconnect
// Adding resource hints to index.html
function ResourceHints() {
return (
<head>
{/* Preconnect to API */}
<link rel="preconnect" href="https://api.example.com" />
{/* DNS prefetch for third-party services */}
<link rel="dns-prefetch" href="https://analytics.example.com" />
{/* Preload critical assets */}
<link
rel="preload"
href="/static/js/main.chunk.js"
as="script"
/>
{/* Prefetch next page resources */}
<link
rel="prefetch"
href="/static/js/dashboard.chunk.js"
as="script"
/>
</head>
);
}
// Dynamic resource hints
function DynamicResourceHints({ nextPage }) {
useEffect(() => {
// Add prefetch hint dynamically
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = `/static/js/${nextPage}.chunk.js`;
link.as = 'script';
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, [nextPage]);
return null;
}
Real-World Implementation Examples
1. E-commerce Application
// E-commerce app with optimized loading
const ProductList = React.lazy(() => import('./pages/ProductList'));
const ProductDetail = React.lazy(() => import('./pages/ProductDetail'));
const Cart = React.lazy(() => import('./pages/Cart'));
const Checkout = React.lazy(() => import('./pages/Checkout'));
// Preload checkout when items are added to cart
function CartButton({ itemCount, onClick }) {
const handleClick = () => {
onClick();
// Preload checkout page when user shows purchase intent
if (itemCount > 0) {
import('./pages/Checkout');
}
};
return (
<button onClick={handleClick}>
Cart ({itemCount})
</button>
);
}
// Product page with progressive loading
function ProductPage({ productId }) {
const [product, setProduct] = useState(null);
const [showReviews, setShowReviews] = useState(false);
// Load product data first
useEffect(() => {
fetchProduct(productId).then(setProduct);
}, [productId]);
// Lazy load heavy components
const ProductGallery = React.lazy(() => import('./components/ProductGallery'));
const ProductReviews = React.lazy(() => import('./components/ProductReviews'));
const RelatedProducts = React.lazy(() => import('./components/RelatedProducts'));
if (!product) return <LoadingSpinner />;
return (
<div className="product-page">
{/* Critical content loads immediately */}
<h1>{product.name}</h1>
<p>{product.price}</p>
<button>Add to Cart</button>
{/* Gallery loads when visible */}
<Suspense fallback={<div>Loading gallery...</div>}>
<ProductGallery images={product.images} />
</Suspense>
{/* Reviews load on demand */}
<button onClick={() => setShowReviews(true)}>
Show Reviews ({product.reviewCount})
</button>
{showReviews && (
<Suspense fallback={<div>Loading reviews...</div>}>
<ProductReviews productId={productId} />
</Suspense>
)}
{/* Related products load after main content */}
<Suspense fallback={<div>Loading related products...</div>}>
<RelatedProducts category={product.category} />
</Suspense>
</div>
);
}
2. Dashboard Application
// Dashboard with progressive enhancement
function Dashboard() {
const [user, setUser] = useState(null);
const [activeWidgets, setActiveWidgets] = useState(['summary']);
// Lazy load dashboard widgets
const widgets = {
summary: React.lazy(() => import('./widgets/Summary')),
analytics: React.lazy(() => import('./widgets/Analytics')),
reports: React.lazy(() => import('./widgets/Reports')),
calendar: React.lazy(() => import('./widgets/Calendar')),
tasks: React.lazy(() => import('./widgets/Tasks')),
};
// Preload commonly used widgets
useEffect(() => {
// Preload analytics and reports for power users
if (user?.isPowerUser) {
import('./widgets/Analytics');
import('./widgets/Reports');
}
}, [user]);
const addWidget = (widgetName) => {
setActiveWidgets(prev => [...prev, widgetName]);
};
return (
<div className="dashboard">
<h1>Dashboard</h1>
<div className="widget-selector">
{Object.keys(widgets).map(widgetName => (
<button
key={widgetName}
onClick={() => addWidget(widgetName)}
disabled={activeWidgets.includes(widgetName)}
>
Add {widgetName}
</button>
))}
</div>
<div className="widgets-grid">
{activeWidgets.map(widgetName => {
const Widget = widgets[widgetName];
return (
<div key={widgetName} className="widget-container">
<ErrorBoundary>
<Suspense fallback={<WidgetSkeleton />}>
<Widget />
</Suspense>
</ErrorBoundary>
</div>
);
})}
</div>
</div>
);
}
// Widget skeleton for loading states
function WidgetSkeleton() {
return (
<div className="widget-skeleton">
<div className="skeleton-header" />
<div className="skeleton-content">
<div className="skeleton-line" />
<div className="skeleton-line" />
<div className="skeleton-line" />
</div>
</div>
);
}
3. Content Management System
// CMS with role-based code splitting
function CMS() {
const { user } = useAuth();
// Load components based on user role
const getEditorComponent = () => {
switch (user.role) {
case 'admin':
return React.lazy(() => import('./editors/AdminEditor'));
case 'editor':
return React.lazy(() => import('./editors/ContentEditor'));
case 'author':
return React.lazy(() => import('./editors/BasicEditor'));
default:
return React.lazy(() => import('./editors/ViewOnly'));
}
};
const Editor = getEditorComponent();
// Preload plugins based on user preferences
useEffect(() => {
if (user.preferences.includes('markdown')) {
import('./plugins/MarkdownPlugin');
}
if (user.preferences.includes('codeHighlight')) {
import('./plugins/CodeHighlightPlugin');
}
}, [user.preferences]);
return (
<div className="cms">
<Suspense fallback={<EditorSkeleton />}>
<Editor />
</Suspense>
</div>
);
}
// Plugin system with dynamic loading
function PluginSystem({ content, enabledPlugins }) {
const [plugins, setPlugins] = useState({});
useEffect(() => {
const loadPlugins = async () => {
const loadedPlugins = {};
for (const pluginName of enabledPlugins) {
try {
const module = await import(`./plugins/${pluginName}`);
loadedPlugins[pluginName] = module.default;
} catch (error) {
console.error(`Failed to load plugin: ${pluginName}`, error);
}
}
setPlugins(loadedPlugins);
};
loadPlugins();
}, [enabledPlugins]);
// Apply plugins to content
let processedContent = content;
Object.values(plugins).forEach(plugin => {
if (plugin.process) {
processedContent = plugin.process(processedContent);
}
});
return (
<div className="plugin-system">
{processedContent}
</div>
);
}
Best Practices
1. Strategic Code Splitting
// Split at route boundaries
const routes = [
{
path: '/',
component: React.lazy(() => import('./pages/Home')),
},
{
path: '/dashboard',
component: React.lazy(() => import('./pages/Dashboard')),
},
];
// Split heavy components
const HeavyChart = React.lazy(() => import('./components/HeavyChart'));
// Split by feature/module
const AdminModule = React.lazy(() => import('./modules/admin'));
// Don't split small components
// Bad: const Button = React.lazy(() => import('./Button'));
// Good: import Button from './Button';
2. Meaningful Loading States
// Generic loading
<Suspense fallback={<div>Loading...</div>}>
// Contextual loading
<Suspense fallback={<ChartSkeleton />}>
<AnalyticsChart />
</Suspense>
// Progressive loading UI
function LoadingProgress({ stage }) {
return (
<div className="loading-progress">
<div className="progress-bar" />
<p>{stage}</p>
</div>
);
}
3. Error Handling
// Always wrap lazy components with error boundaries
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
// Implement retry logic for failed imports
const RetryableLazy = ({ component: Component }) => {
const [hasError, setHasError] = useState(false);
if (hasError) {
return (
<button onClick={() => setHasError(false)}>
Retry Loading
</button>
);
}
return (
<ErrorBoundary onError={() => setHasError(true)}>
<Component />
</ErrorBoundary>
);
};
Practice Exercises
Exercise 1: Optimize an E-commerce App
Create an e-commerce application with:
- Route-based code splitting
- Progressive loading of product images
- Lazy loading of reviews and recommendations
- Preloading checkout when items are in cart
Exercise 2: Build a Dashboard
Create a dashboard with:
- Lazy loaded widgets
- Role-based component loading
- Progressive enhancement
- Error boundaries for each widget
Exercise 3: Implement a Media Gallery
Build a media gallery that:
- Lazy loads images as they come into view
- Preloads adjacent images
- Implements progressive image loading
- Handles loading errors gracefully
Key Takeaways
- Code splitting reduces initial bundle size and improves load time
- Use React.lazy and Suspense for component-level code splitting
- Implement route-based splitting for large applications
- Always provide meaningful loading states
- Use error boundaries to handle loading failures
- Preload components based on user behavior
- Monitor bundle sizes with tools like webpack-bundle-analyzer