Code splitting and lazy loading are essential for optimizing application performance. This article covers techniques for reducing initial bundle size and loading code on-demand.
Introduction
Large JavaScript bundles cause:
- Slow initial page load
- High bandwidth usage
- Poor performance on slow networks
- Wasted resources on unused code
Code splitting helps you:
- Reduce initial bundle size
- Load code on-demand
- Improve Time to Interactive (TTI)
- Optimize for different user scenarios
Dynamic Imports
Basic Dynamic Import
// Static import (loaded immediately)
import { heavyModule } from './heavy-module.js';
// Dynamic import (loaded on-demand)
import('./heavy-module.js').then(module => {
module.doSomething();
});
// Using async/await
async function loadModule() {
const module = await import('./heavy-module.js');
module.doSomething();
}
// Call when needed
loadModule();
Conditional Imports
// Load different modules based on conditions
async function loadModule(type) {
let module;
if (type === 'advanced') {
module = await import('./advanced-features.js');
} else {
module = await import('./basic-features.js');
}
return module;
}
// Usage
const features = await loadModule('advanced');
// Feature detection
async function loadPolyfill() {
if (!window.Promise) {
await import('./promise-polyfill.js');
}
}
loadPolyfill();
Error Handling
// Handle import errors
async function safeImport(modulePath) {
try {
return await import(modulePath);
} catch (error) {
console.error(`Failed to load ${modulePath}:`, error);
// Load fallback
return await import('./fallback-module.js');
}
}
// Usage
const module = await safeImport('./optional-feature.js');
Route-Based Code Splitting
React Router Example
// ❌ Bad: All routes loaded upfront
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/contact', component: Contact }
];
// ✅ Good: Route-based code splitting
import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/contact', component: Contact }
];
// Wrap routes with Suspense
function App() {
return (
<Suspense fallback={<Loading />}>
<Routes>
{routes.map(route => (
<Route key={route.path} path={route.path} element={<route.component />} />
))}
</Routes>
</Suspense>
);
}
Vue Router Example
// ✅ Good: Vue route-based code splitting
const routes = [
{
path: '/',
component: () => import('./pages/Home.vue')
},
{
path: '/about',
component: () => import('./pages/About.vue')
},
{
path: '/contact',
component: () => import('./pages/Contact.vue')
}
];
Component-Based Code Splitting
React Lazy Components
// ✅ Good: Lazy load components
import { lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
);
}
// Practical example: Modal component
import { lazy, Suspense, useState } from 'react';
const Modal = lazy(() => import('./Modal'));
function App() {
const [showModal, setShowModal] = useState(false);
return (
<>
<button onClick={() => setShowModal(true)}>Open Modal</button>
{showModal && (
<Suspense fallback={<div>Loading modal...</div>}>
<Modal onClose={() => setShowModal(false)} />
</Suspense>
)}
</>
);
}
Webpack Code Splitting
Entry Points
// webpack.config.js
module.exports = {
entry: {
main: './src/index.js',
admin: './src/admin.js',
vendor: './src/vendor.js'
},
output: {
filename: '[name].bundle.js'
}
};
// Generates:
// - main.bundle.js
// - admin.bundle.js
// - vendor.bundle.js
SplitChunksPlugin
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// Vendor libraries
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
},
// Common code used in multiple chunks
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
Dynamic Imports with Webpack
// Webpack automatically creates chunks for dynamic imports
async function loadFeature() {
const module = await import(/* webpackChunkName: "feature" */ './feature.js');
return module;
}
// Generates: feature.bundle.js
Lazy Loading Strategies
Intersection Observer for Images
// ✅ Good: Lazy load images
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.classList.add('loaded');
imageObserver.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
// HTML:
// <img data-src="image.jpg" src="placeholder.jpg" />
Lazy Load Heavy Libraries
// ✅ Good: Load heavy libraries on-demand
class ChartManager {
async renderChart(data) {
// Load chart library only when needed
const Chart = (await import('chart.js')).default;
const ctx = document.getElementById('chart').getContext('2d');
new Chart(ctx, {
type: 'bar',
data: data
});
}
}
// Usage
const manager = new ChartManager();
manager.renderChart(data); // Chart.js loaded on first use
Prefetching and Preloading
// Prefetch: Load resource in background (low priority)
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = '/next-page.js';
document.head.appendChild(link);
// Preload: Load resource with high priority
const preloadLink = document.createElement('link');
preloadLink.rel = 'preload';
preloadLink.as = 'script';
preloadLink.href = '/critical-module.js';
document.head.appendChild(preloadLink);
// Webpack magic comments
// Prefetch
import(/* webpackPrefetch: true */ './next-page.js');
// Preload
import(/* webpackPreload: true */ './critical-module.js');
Practical Patterns
Progressive Enhancement
// ✅ Good: Load features progressively
class FeatureManager {
constructor() {
this.features = new Map();
}
async loadFeature(name) {
if (this.features.has(name)) {
return this.features.get(name);
}
try {
const module = await import(`./features/${name}.js`);
this.features.set(name, module);
return module;
} catch (error) {
console.error(`Failed to load feature ${name}:`, error);
return null;
}
}
async enableFeature(name) {
const feature = await this.loadFeature(name);
if (feature) {
feature.enable();
}
}
}
// Usage
const manager = new FeatureManager();
manager.enableFeature('advanced-search');
Bundle Analysis
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
]
};
// Run: webpack --mode production
// View: bundle-report.html
Monitoring Bundle Size
// Check bundle size in CI/CD
const fs = require('fs');
const path = require('path');
function checkBundleSize() {
const bundlePath = path.join(__dirname, 'dist/main.bundle.js');
const stats = fs.statSync(bundlePath);
const sizeInKB = stats.size / 1024;
const maxSize = 250; // KB
if (sizeInKB > maxSize) {
console.error(`Bundle size ${sizeInKB.toFixed(2)}KB exceeds limit ${maxSize}KB`);
process.exit(1);
}
console.log(`Bundle size: ${sizeInKB.toFixed(2)}KB (limit: ${maxSize}KB)`);
}
checkBundleSize();
Performance Optimization
Measuring Impact
// Measure code splitting impact
class PerformanceMonitor {
measureImport(modulePath) {
const startTime = performance.now();
return import(modulePath).then(module => {
const duration = performance.now() - startTime;
console.log(`Loaded ${modulePath} in ${duration.toFixed(2)}ms`);
return module;
});
}
}
// Usage
const monitor = new PerformanceMonitor();
monitor.measureImport('./heavy-module.js');
Caching Strategies
// ✅ Good: Cache loaded modules
class ModuleCache {
constructor() {
this.cache = new Map();
}
async load(modulePath) {
if (this.cache.has(modulePath)) {
return this.cache.get(modulePath);
}
const module = await import(modulePath);
this.cache.set(modulePath, module);
return module;
}
clear() {
this.cache.clear();
}
}
// Usage
const cache = new ModuleCache();
const module1 = await cache.load('./module.js'); // Loaded
const module2 = await cache.load('./module.js'); // Cached
Best Practices
- Split by route:
// ✅ Good const Home = lazy(() => import('./pages/Home')); ```javascript - Split by feature:
// ✅ Good const AdvancedSearch = lazy(() => import('./features/AdvancedSearch')); ```javascript - Prefetch next routes:
// ✅ Good import(/* webpackPrefetch: true */ './next-page.js'); ```javascript - Monitor bundle size:
// ✅ Good checkBundleSize(); // In CI/CD ```javascript - Use Suspense for loading states:
// ✅ Good <Suspense fallback={<Loading />}> <LazyComponent /> </Suspense> ```javascript
Common Mistakes
- Not splitting at all:
// ❌ Bad - single large bundle // ✅ Good - split by route/feature ```javascript - Over-splitting:
// ❌ Bad - too many small chunks // ✅ Good - balanced chunk sizes ```javascript - Not handling loading states:
// ❌ Bad const Component = lazy(() => import('./Component')); // ✅ Good <Suspense fallback={<Loading />}> <Component /> </Suspense> ```javascript - Ignoring bundle analysis:
// ❌ Bad - don't know what's in bundle // ✅ Good - analyze with BundleAnalyzerPlugin
Summary
Code splitting and lazy loading are essential for performance. Key takeaways:
- Use dynamic imports for on-demand loading
- Split by route and feature
- Use Intersection Observer for lazy loading
- Monitor bundle size
- Implement proper loading states
- Cache loaded modules
- Analyze bundle composition
- Prefetch/preload critical resources
Related Resources
- Code Splitting - Webpack
- Dynamic Imports - MDN
- React Code Splitting
- Intersection Observer - MDN
- Bundle Analysis - Webpack
Next Steps
- Learn about Caching Strategies: Client, Server, CDN
- Explore Bundle Optimization and Tree Shaking
- Study Performance Profiling: DevTools and Metrics
- Implement code splitting in your projects
- Monitor bundle size in CI/CD
Comments