Back to Blog
ReactLazy LoadingPerformanceCode Splitting

React Lazy Loading Components

Master React.lazy and Suspense for code splitting and performance optimization.

B
Bootspring Team
Engineering
October 15, 2018
7 min read

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}
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.

Share this article

Help spread the word about Bootspring