Code Splitting
Reduce initial bundle size with lazy loading.
Route-Based Splitting
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// Lazy load route components
const Dashboard = lazy(() => import('@/features/dashboard'));
const Users = lazy(() => import('@/features/users'));
const Settings = lazy(() => import('@/features/settings'));
const PageSkeleton = () => (
<div className="animate-pulse p-8">
<div className="h-8 w-48 bg-gray-200 rounded mb-4" />
<div className="h-64 bg-gray-200 rounded" />
</div>
);
const AppRoutes = () => (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/users/*" element={<Users />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);Component-Level Splitting
const HeavyChart = lazy(() => import('./HeavyChart'));
const DataExporter = lazy(() => import('./DataExporter'));
const Dashboard = () => {
const [showExporter, setShowExporter] = useState(false);
return (
<div>
<h1>Dashboard</h1>
{/* Load chart when visible */}
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={chartData} />
</Suspense>
{/* Load exporter on demand */}
<button onClick={() => setShowExporter(true)}>
Export Data
</button>
{showExporter && (
<Suspense fallback={<Spinner />}>
<DataExporter />
</Suspense>
)}
</div>
);
};Named Exports with Lazy
// For named exports, use intermediate module
// features/users/index.ts
export { UserList } from './components/UserList';
export { UserProfile } from './components/UserProfile';
// Lazy load
const Users = lazy(() =>
import('@/features/users').then((module) => ({
default: module.UserList,
}))
);Preloading
// Preload on hover
const UserLink = ({ userId }: { userId: string }) => {
const preloadProfile = () => {
import('@/features/users/UserProfile');
};
return (
<Link
to={`/users/${userId}`}
onMouseEnter={preloadProfile}
>
View Profile
</Link>
);
};
// Preload after initial load
useEffect(() => {
// Preload likely next routes
const timer = setTimeout(() => {
import('@/features/settings');
}, 2000);
return () => clearTimeout(timer);
}, []);Vite Configuration
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunks
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-vendor': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
'query-vendor': ['@tanstack/react-query'],
},
},
},
// Chunk size warning
chunkSizeWarningLimit: 500,
},
});Bundle Analysis
# Install analyzer
pnpm add -D rollup-plugin-visualizer
# Add to vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
filename: 'stats.html',
open: true,
}),
],
});
# Build and analyze
pnpm buildLoading States
// Skeleton components for each feature
const DashboardSkeleton = () => (
<div className="animate-pulse space-y-4">
<div className="h-8 w-64 bg-gray-200 rounded" />
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-32 bg-gray-200 rounded" />
))}
</div>
</div>
);
// Use specific skeletons
<Suspense fallback={<DashboardSkeleton />}>
<Dashboard />
</Suspense>Error Handling
import { ErrorBoundary } from 'react-error-boundary';
const LazyComponent = lazy(() => import('./HeavyComponent'));
<ErrorBoundary
fallback={<div>Failed to load component</div>}
onError={(error) => console.error('Lazy load failed:', error)}
>
<Suspense fallback={<Skeleton />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>