Optimizing React App Performance with Code Splitting
Introduction
Code splitting reduces initial bundle size and improves loading performance by loading code on-demand. This guide covers various splitting strategies in React.
Prerequisites
- React 18+
- Webpack/Vite bundler
- React Router (optional)
Step 1: Component-Level Code Splitting
Create components/LazyComponents.tsx
:
import { lazy, Suspense } from 'react';
// Lazy load components
const HeavyChart = lazy(() => import('./HeavyChart'));
const DataTable = lazy(() => import('./DataTable'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));
// Loading fallback component
const LoadingSpinner = () => (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2">Loading component...</span>
</div>
);
export default function LazyComponents() {
return (
<div className="space-y-8">
<div>
<h2 className="text-xl font-semibold mb-4">Heavy Chart Component</h2>
<Suspense fallback={<LoadingSpinner />}>
<HeavyChart /> {/* Only loads when rendered */}
</Suspense>
</div>
<div>
<h2 className="text-xl font-semibold mb-4">Data Table</h2>
<Suspense fallback={<LoadingSpinner />}>
<DataTable />
</Suspense>
</div>
<div>
<h2 className="text-xl font-semibold mb-4">Video Player</h2>
<Suspense fallback={<LoadingSpinner />}>
<VideoPlayer />
</Suspense>
</div>
</div>
);
}
Step 2: Route-Based Code Splitting
Create router/AppRouter.tsx
:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
// Lazy load route components
const Home = lazy(() => import('../pages/Home'));
const Dashboard = lazy(() => import('../pages/Dashboard'));
const Profile = lazy(() => import('../pages/Profile'));
const Settings = lazy(() => import('../pages/Settings'));
// Route loading component
const RouteLoader = () => (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600">Loading page...</p>
</div>
</div>
);
export default function AppRouter() {
return (
<BrowserRouter>
<Suspense fallback={<RouteLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Step 3: Dynamic Import with Conditions
Create components/ConditionalLoader.tsx
:
import { useState, Suspense } from 'react';
interface DynamicComponentProps {
type: 'chart' | 'table' | 'calendar';
}
function DynamicComponent({ type }: DynamicComponentProps) {
// Dynamic import based on type
const [Component, setComponent] = useState<React.ComponentType | null>(null);
const loadComponent = async () => {
let ImportedComponent;
switch (type) {
case 'chart':
ImportedComponent = await import('./ChartComponent');
break;
case 'table':
ImportedComponent = await import('./TableComponent');
break;
case 'calendar':
ImportedComponent = await import('./CalendarComponent');
break;
default:
return;
}
setComponent(() => ImportedComponent.default);
};
if (!Component) {
return (
<button
onClick={loadComponent}
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Load {type.charAt(0).toUpperCase() + type.slice(1)} Component
</button>
);
}
return (
<Suspense fallback={<div>Loading {type}...</div>}>
<Component />
</Suspense>
);
}
export default function ConditionalLoader() {
return (
<div className="space-y-6 p-6">
<h2 className="text-2xl font-bold">Conditional Component Loading</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-4">Chart Component</h3>
<DynamicComponent type="chart" />
</div>
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-4">Table Component</h3>
<DynamicComponent type="table" />
</div>
<div className="border rounded-lg p-4">
<h3 className="font-semibold mb-4">Calendar Component</h3>
<DynamicComponent type="calendar" />
</div>
</div>
</div>
);
}
Step 4: Library Code Splitting
Create utils/dynamicImports.ts
:
// Split large libraries into separate chunks
export const loadMoment = async () => {
const moment = await import("moment");
return moment.default;
};
export const loadLodash = async () => {
const _ = await import("lodash");
return _;
};
export const loadChartJS = async () => {
const { Chart, registerables } = await import("chart.js");
Chart.register(...registerables);
return Chart;
};
// Utility function for conditional library loading
export const loadLibraryOnDemand = async <T>(
libraryName: string,
loader: () => Promise<T>
): Promise<T> => {
console.log(`Loading ${libraryName}...`);
const startTime = performance.now();
try {
const library = await loader();
const loadTime = performance.now() - startTime;
console.log(`${libraryName} loaded in ${loadTime.toFixed(2)}ms`);
return library;
} catch (error) {
console.error(`Failed to load ${libraryName}:`, error);
throw error;
}
};
Step 5: Advanced Preloading Strategies
Create hooks/usePreload.ts
:
import { useEffect, useState } from 'react';
interface PreloadOptions {
delay?: number;
condition?: boolean;
}
export function usePreload(
importFn: () => Promise<any>,
{ delay = 0, condition = true }: PreloadOptions = {}
) {
const [isLoaded, setIsLoaded] = useState(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
if (!condition) return;
const preload = async () => {
try {
await new Promise(resolve => setTimeout(resolve, delay));
await importFn();
setIsLoaded(true);
} catch (err) {
setError(err instanceof Error ? err : new Error('Preload failed'));
}
};
preload();
}, [importFn, delay, condition]);
return { isLoaded, error };
}
// Usage component
export function PreloadingExample() {
const [showChart, setShowChart] = useState(false);
// Preload chart component after 2 seconds
const { isLoaded } = usePreload(
() => import('../components/HeavyChart'),
{ delay: 2000 }
);
return (
<div className="p-6">
<div className="mb-4">
<span className={`inline-block w-3 h-3 rounded-full mr-2 ${
isLoaded ? 'bg-green-500' : 'bg-yellow-500'
}`}></span>
Chart component {isLoaded ? 'preloaded' : 'loading...'}
</div>
<button
onClick={() => setShowChart(!showChart)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{showChart ? 'Hide' : 'Show'} Chart
</button>
{showChart && (
<div className="mt-4">
<Suspense fallback={<div>Loading chart...</div>}>
<LazyChart />
</Suspense>
</div>
)}
</div>
);
}
const LazyChart = lazy(() => import('../components/HeavyChart'));
Step 6: Bundle Analysis and Monitoring
Create utils/bundleAnalytics.ts
:
// Monitor chunk loading performance
export class ChunkLoadMonitor {
private static loadTimes = new Map<string, number>();
static startLoading(chunkName: string) {
this.loadTimes.set(chunkName, performance.now());
}
static endLoading(chunkName: string) {
const startTime = this.loadTimes.get(chunkName);
if (startTime) {
const loadTime = performance.now() - startTime;
console.log(`Chunk "${chunkName}" loaded in ${loadTime.toFixed(2)}ms`);
// Send to analytics
if (typeof window !== "undefined" && "gtag" in window) {
(window as any).gtag("event", "chunk_load", {
chunk_name: chunkName,
load_time: Math.round(loadTime),
});
}
this.loadTimes.delete(chunkName);
}
}
static getStats() {
return {
pendingChunks: Array.from(this.loadTimes.keys()),
pendingCount: this.loadTimes.size,
};
}
}
// Enhanced lazy loading with monitoring
export function createMonitoredLazy<T extends React.ComponentType<any>>(
importFn: () => Promise<{ default: T }>,
chunkName: string
) {
return lazy(async () => {
ChunkLoadMonitor.startLoading(chunkName);
try {
const module = await importFn();
ChunkLoadMonitor.endLoading(chunkName);
return module;
} catch (error) {
ChunkLoadMonitor.endLoading(chunkName);
throw error;
}
});
}
Step 7: Production Bundle Configuration
Create webpack.config.js
(if using custom Webpack):
module.exports = {
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all",
},
common: {
name: "common",
minChunks: 2,
priority: -10,
reuseExistingChunk: true,
},
},
},
},
};
For Vite, update vite.config.ts
:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ["react", "react-dom"],
router: ["react-router-dom"],
ui: ["framer-motion", "@headlessui/react"],
},
},
},
},
});
Summary
Code splitting with React.lazy(), Suspense, and dynamic imports significantly improves initial load performance. Combine route-based, component-based, and library splitting for optimal results.