React.lazy and Suspense enable code splitting for better performance. Here's how to use them effectively.
Basic Lazy Loading#
1import { lazy, Suspense } from 'react';
2
3// Lazy load component
4const HeavyComponent = lazy(() => import('./HeavyComponent'));
5
6function App() {
7 return (
8 <Suspense fallback={<div>Loading...</div>}>
9 <HeavyComponent />
10 </Suspense>
11 );
12}
13
14// Without lazy loading - bundled immediately
15// import HeavyComponent from './HeavyComponent';Route-Based Code Splitting#
1import { lazy, Suspense } from 'react';
2import { BrowserRouter, Routes, Route } from 'react-router-dom';
3
4// Lazy load route components
5const Home = lazy(() => import('./pages/Home'));
6const About = lazy(() => import('./pages/About'));
7const Dashboard = lazy(() => import('./pages/Dashboard'));
8const Settings = lazy(() => import('./pages/Settings'));
9
10function App() {
11 return (
12 <BrowserRouter>
13 <Suspense fallback={<PageLoader />}>
14 <Routes>
15 <Route path="/" element={<Home />} />
16 <Route path="/about" element={<About />} />
17 <Route path="/dashboard" element={<Dashboard />} />
18 <Route path="/settings" element={<Settings />} />
19 </Routes>
20 </Suspense>
21 </BrowserRouter>
22 );
23}
24
25function PageLoader() {
26 return (
27 <div className="page-loader">
28 <Spinner />
29 <p>Loading page...</p>
30 </div>
31 );
32}Multiple Suspense Boundaries#
1import { lazy, Suspense } from 'react';
2
3const Header = lazy(() => import('./Header'));
4const Sidebar = lazy(() => import('./Sidebar'));
5const MainContent = lazy(() => import('./MainContent'));
6const Footer = lazy(() => import('./Footer'));
7
8function App() {
9 return (
10 <div className="app">
11 {/* Critical - load first */}
12 <Suspense fallback={<HeaderSkeleton />}>
13 <Header />
14 </Suspense>
15
16 <div className="content">
17 {/* Non-critical - can load independently */}
18 <Suspense fallback={<SidebarSkeleton />}>
19 <Sidebar />
20 </Suspense>
21
22 <Suspense fallback={<ContentSkeleton />}>
23 <MainContent />
24 </Suspense>
25 </div>
26
27 <Suspense fallback={null}>
28 <Footer />
29 </Suspense>
30 </div>
31 );
32}Named Exports#
1// Component file with named export
2// components/Charts.js
3export const LineChart = () => { /* ... */ };
4export const BarChart = () => { /* ... */ };
5export const PieChart = () => { /* ... */ };
6
7// Lazy loading named exports
8const LineChart = lazy(() =>
9 import('./components/Charts').then(module => ({
10 default: module.LineChart
11 }))
12);
13
14const BarChart = lazy(() =>
15 import('./components/Charts').then(module => ({
16 default: module.BarChart
17 }))
18);
19
20// Or create a wrapper
21// chartLoaders.js
22export const LazyLineChart = lazy(() =>
23 import('./Charts').then(m => ({ default: m.LineChart }))
24);Preloading Components#
1import { lazy, Suspense } from 'react';
2
3// Create lazy component
4const HeavyFeature = lazy(() => import('./HeavyFeature'));
5
6// Preload function
7const preloadHeavyFeature = () => import('./HeavyFeature');
8
9function App() {
10 return (
11 <div>
12 {/* Preload on hover */}
13 <button
14 onMouseEnter={preloadHeavyFeature}
15 onClick={() => setShowFeature(true)}
16 >
17 Show Feature
18 </button>
19
20 {showFeature && (
21 <Suspense fallback={<Loading />}>
22 <HeavyFeature />
23 </Suspense>
24 )}
25 </div>
26 );
27}
28
29// Preload on route link hover
30function NavLink({ to, children, component }) {
31 const preload = () => {
32 if (component) {
33 component.preload?.();
34 }
35 };
36
37 return (
38 <Link to={to} onMouseEnter={preload}>
39 {children}
40 </Link>
41 );
42}Error Handling#
1import { lazy, Suspense, Component } from 'react';
2
3// Error Boundary
4class ErrorBoundary extends Component {
5 state = { hasError: false, error: null };
6
7 static getDerivedStateFromError(error) {
8 return { hasError: true, error };
9 }
10
11 retry = () => {
12 this.setState({ hasError: false, error: null });
13 };
14
15 render() {
16 if (this.state.hasError) {
17 return (
18 <div className="error">
19 <p>Failed to load component</p>
20 <button onClick={this.retry}>Retry</button>
21 </div>
22 );
23 }
24 return this.props.children;
25 }
26}
27
28// Wrap lazy components
29function App() {
30 return (
31 <ErrorBoundary>
32 <Suspense fallback={<Loading />}>
33 <LazyComponent />
34 </Suspense>
35 </ErrorBoundary>
36 );
37}
38
39// Retry logic with lazy
40function lazyWithRetry(importFn, retries = 3) {
41 return lazy(async () => {
42 for (let i = 0; i < retries; i++) {
43 try {
44 return await importFn();
45 } catch (error) {
46 if (i === retries - 1) throw error;
47 await new Promise(r => setTimeout(r, 1000 * (i + 1)));
48 }
49 }
50 });
51}
52
53const ReliableComponent = lazyWithRetry(() => import('./Component'));Conditional Loading#
1import { lazy, Suspense, useState } from 'react';
2
3// Only load when needed
4const AdminPanel = lazy(() => import('./AdminPanel'));
5const UserPanel = lazy(() => import('./UserPanel'));
6
7function Dashboard({ user }) {
8 return (
9 <Suspense fallback={<DashboardSkeleton />}>
10 {user.isAdmin ? <AdminPanel /> : <UserPanel />}
11 </Suspense>
12 );
13}
14
15// Feature flag loading
16const NewFeature = lazy(() => import('./NewFeature'));
17const LegacyFeature = lazy(() => import('./LegacyFeature'));
18
19function Feature({ flags }) {
20 const Component = flags.newFeature ? NewFeature : LegacyFeature;
21
22 return (
23 <Suspense fallback={<Loading />}>
24 <Component />
25 </Suspense>
26 );
27}Modal/Dialog Loading#
1import { lazy, Suspense, useState } from 'react';
2
3// Heavy modal loaded on demand
4const SettingsModal = lazy(() => import('./SettingsModal'));
5
6function App() {
7 const [showSettings, setShowSettings] = useState(false);
8
9 return (
10 <div>
11 <button onClick={() => setShowSettings(true)}>
12 Open Settings
13 </button>
14
15 {showSettings && (
16 <Suspense fallback={<ModalLoader />}>
17 <SettingsModal onClose={() => setShowSettings(false)} />
18 </Suspense>
19 )}
20 </div>
21 );
22}
23
24// Preload pattern for modals
25const preloadSettingsModal = () => import('./SettingsModal');
26
27function SettingsButton() {
28 return (
29 <button
30 onMouseEnter={preloadSettingsModal}
31 onFocus={preloadSettingsModal}
32 onClick={() => setShowSettings(true)}
33 >
34 Settings
35 </button>
36 );
37}Skeleton Loading#
1import { lazy, Suspense } from 'react';
2
3// Skeleton component matching content shape
4function CardSkeleton() {
5 return (
6 <div className="card skeleton">
7 <div className="skeleton-image" />
8 <div className="skeleton-text" />
9 <div className="skeleton-text short" />
10 </div>
11 );
12}
13
14function CardListSkeleton({ count = 3 }) {
15 return (
16 <div className="card-list">
17 {Array.from({ length: count }, (_, i) => (
18 <CardSkeleton key={i} />
19 ))}
20 </div>
21 );
22}
23
24const CardList = lazy(() => import('./CardList'));
25
26function Dashboard() {
27 return (
28 <Suspense fallback={<CardListSkeleton count={6} />}>
29 <CardList />
30 </Suspense>
31 );
32}Code Splitting Strategies#
1// 1. Route-based (most common)
2const routes = {
3 '/': lazy(() => import('./pages/Home')),
4 '/about': lazy(() => import('./pages/About')),
5};
6
7// 2. Component-based (large components)
8const RichTextEditor = lazy(() => import('./RichTextEditor'));
9const DataVisualization = lazy(() => import('./DataVisualization'));
10
11// 3. Feature-based (feature flags)
12const BetaFeature = lazy(() => import('./features/BetaFeature'));
13
14// 4. Vendor splitting (heavy libraries)
15const ChartWithD3 = lazy(() => import('./ChartWithD3'));
16// This creates separate chunk with d3 included
17
18// 5. Below-the-fold content
19const BelowFold = lazy(() => import('./BelowFoldContent'));
20
21function Page() {
22 return (
23 <>
24 <AboveFoldContent />
25 <Suspense fallback={null}>
26 <BelowFold />
27 </Suspense>
28 </>
29 );
30}Webpack Magic Comments#
1// Named chunks
2const Component = lazy(() =>
3 import(/* webpackChunkName: "feature-a" */ './FeatureA')
4);
5
6// Prefetch (load in idle time)
7const Component = lazy(() =>
8 import(/* webpackPrefetch: true */ './HeavyComponent')
9);
10
11// Preload (load with parent)
12const Component = lazy(() =>
13 import(/* webpackPreload: true */ './CriticalComponent')
14);
15
16// Combine options
17const Analytics = lazy(() =>
18 import(
19 /* webpackChunkName: "analytics" */
20 /* webpackPrefetch: true */
21 './Analytics'
22 )
23);Testing Lazy Components#
1import { render, screen, waitFor } from '@testing-library/react';
2import { Suspense } from 'react';
3
4// Mock the lazy import
5jest.mock('./HeavyComponent', () => ({
6 __esModule: true,
7 default: () => <div>Heavy Component Content</div>,
8}));
9
10const LazyComponent = lazy(() => import('./HeavyComponent'));
11
12test('renders lazy component', async () => {
13 render(
14 <Suspense fallback={<div>Loading...</div>}>
15 <LazyComponent />
16 </Suspense>
17 );
18
19 // Initially shows fallback
20 expect(screen.getByText('Loading...')).toBeInTheDocument();
21
22 // Wait for component to load
23 await waitFor(() => {
24 expect(screen.getByText('Heavy Component Content')).toBeInTheDocument();
25 });
26});Best Practices#
When to Use:
✓ Route components
✓ Large components (>30KB)
✓ Conditionally rendered content
✓ Below-the-fold content
Suspense Boundaries:
✓ Group related components
✓ Place near lazy components
✓ Provide meaningful fallbacks
✓ Consider UX of loading states
Performance:
✓ Preload on hover/focus
✓ Use webpackPrefetch
✓ Combine related chunks
✓ Measure bundle sizes
Avoid:
✗ Over-splitting small components
✗ Single global Suspense
✗ Ignoring error boundaries
✗ Too many loading states
Conclusion#
React.lazy and Suspense enable powerful code splitting for better initial load times. Use route-based splitting for page components, lazy load heavy components and modals, and provide meaningful loading states. Preload components on user interaction hints for better perceived performance. Always wrap lazy components with error boundaries for production reliability.