Back to Blog
ReactPerformanceLazy LoadingCode Splitting

React Lazy Loading Guide

Master React lazy loading. From component splitting to route-based loading to images and data.

B
Bootspring Team
Engineering
July 20, 2020
7 min read

Lazy loading improves initial load times by deferring non-critical resources. Here's how to implement it effectively.

React.lazy Basics#

1import { lazy, Suspense } from 'react'; 2 3// Lazy load a component 4const HeavyComponent = lazy(() => import('./HeavyComponent')); 5 6function App() { 7 return ( 8 <Suspense fallback={<LoadingSpinner />}> 9 <HeavyComponent /> 10 </Suspense> 11 ); 12} 13 14// With named exports 15const MyComponent = lazy(() => 16 import('./MyModule').then(module => ({ 17 default: module.MyComponent, 18 })) 19); 20 21// Multiple lazy components 22const Dashboard = lazy(() => import('./Dashboard')); 23const Settings = lazy(() => import('./Settings')); 24const Profile = lazy(() => import('./Profile')); 25 26function App() { 27 return ( 28 <Suspense fallback={<PageLoader />}> 29 <Routes> 30 <Route path="/dashboard" element={<Dashboard />} /> 31 <Route path="/settings" element={<Settings />} /> 32 <Route path="/profile" element={<Profile />} /> 33 </Routes> 34 </Suspense> 35 ); 36}

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 NotFound = lazy(() => import('./pages/NotFound')); 9 10// Loading component 11function PageLoader() { 12 return ( 13 <div className="page-loader"> 14 <div className="spinner" /> 15 <p>Loading...</p> 16 </div> 17 ); 18} 19 20function App() { 21 return ( 22 <BrowserRouter> 23 <Suspense fallback={<PageLoader />}> 24 <Routes> 25 <Route path="/" element={<Home />} /> 26 <Route path="/about" element={<About />} /> 27 <Route path="/dashboard/*" element={<Dashboard />} /> 28 <Route path="*" element={<NotFound />} /> 29 </Routes> 30 </Suspense> 31 </BrowserRouter> 32 ); 33} 34 35// Nested route splitting 36const DashboardOverview = lazy(() => import('./pages/DashboardOverview')); 37const DashboardAnalytics = lazy(() => import('./pages/DashboardAnalytics')); 38const DashboardSettings = lazy(() => import('./pages/DashboardSettings')); 39 40function Dashboard() { 41 return ( 42 <div className="dashboard"> 43 <Sidebar /> 44 <Suspense fallback={<ContentLoader />}> 45 <Routes> 46 <Route index element={<DashboardOverview />} /> 47 <Route path="analytics" element={<DashboardAnalytics />} /> 48 <Route path="settings" element={<DashboardSettings />} /> 49 </Routes> 50 </Suspense> 51 </div> 52 ); 53}

Component-Level Splitting#

1import { lazy, Suspense, useState } from 'react'; 2 3// Lazy load heavy components 4const DataVisualization = lazy(() => import('./DataVisualization')); 5const RichTextEditor = lazy(() => import('./RichTextEditor')); 6const ImageEditor = lazy(() => import('./ImageEditor')); 7 8function App() { 9 const [showEditor, setShowEditor] = useState(false); 10 11 return ( 12 <div> 13 <button onClick={() => setShowEditor(true)}>Open Editor</button> 14 15 {showEditor && ( 16 <Suspense fallback={<EditorSkeleton />}> 17 <RichTextEditor /> 18 </Suspense> 19 )} 20 </div> 21 ); 22} 23 24// Modal with lazy content 25const ModalContent = lazy(() => import('./ModalContent')); 26 27function Modal({ isOpen, onClose }) { 28 if (!isOpen) return null; 29 30 return ( 31 <div className="modal-overlay"> 32 <div className="modal"> 33 <Suspense fallback={<ModalLoader />}> 34 <ModalContent onClose={onClose} /> 35 </Suspense> 36 </div> 37 </div> 38 ); 39}

Preloading Components#

1// Preload function 2const Dashboard = lazy(() => import('./Dashboard')); 3 4// Preload on hover 5function NavLink({ to, children }) { 6 const preload = () => { 7 if (to === '/dashboard') { 8 import('./Dashboard'); 9 } 10 }; 11 12 return ( 13 <Link to={to} onMouseEnter={preload}> 14 {children} 15 </Link> 16 ); 17} 18 19// Preload factory 20function lazyWithPreload<T extends React.ComponentType<any>>( 21 factory: () => Promise<{ default: T }> 22) { 23 const Component = lazy(factory); 24 (Component as any).preload = factory; 25 return Component; 26} 27 28const Settings = lazyWithPreload(() => import('./Settings')); 29 30// Usage 31function Nav() { 32 return ( 33 <nav> 34 <Link 35 to="/settings" 36 onMouseEnter={() => (Settings as any).preload()} 37 > 38 Settings 39 </Link> 40 </nav> 41 ); 42} 43 44// Preload on route change 45import { useLocation } from 'react-router-dom'; 46 47function usePreloadOnRouteChange() { 48 const location = useLocation(); 49 50 useEffect(() => { 51 // Preload likely next routes 52 if (location.pathname === '/') { 53 import('./Dashboard'); 54 } else if (location.pathname === '/dashboard') { 55 import('./Settings'); 56 } 57 }, [location.pathname]); 58}

Lazy Loading Images#

1import { useState, useEffect, useRef } from 'react'; 2 3// Intersection Observer hook 4function useLazyLoad() { 5 const [isVisible, setIsVisible] = useState(false); 6 const ref = useRef<HTMLDivElement>(null); 7 8 useEffect(() => { 9 const observer = new IntersectionObserver( 10 ([entry]) => { 11 if (entry.isIntersecting) { 12 setIsVisible(true); 13 observer.disconnect(); 14 } 15 }, 16 { threshold: 0.1 } 17 ); 18 19 if (ref.current) { 20 observer.observe(ref.current); 21 } 22 23 return () => observer.disconnect(); 24 }, []); 25 26 return { ref, isVisible }; 27} 28 29// Lazy image component 30function LazyImage({ src, alt, placeholder, ...props }) { 31 const { ref, isVisible } = useLazyLoad(); 32 const [loaded, setLoaded] = useState(false); 33 34 return ( 35 <div ref={ref} className="lazy-image-container"> 36 {placeholder && !loaded && ( 37 <img src={placeholder} alt="" className="placeholder" /> 38 )} 39 {isVisible && ( 40 <img 41 src={src} 42 alt={alt} 43 onLoad={() => setLoaded(true)} 44 className={loaded ? 'loaded' : 'loading'} 45 {...props} 46 /> 47 )} 48 </div> 49 ); 50} 51 52// Native lazy loading 53function NativelazyImage({ src, alt, ...props }) { 54 return <img src={src} alt={alt} loading="lazy" {...props} />; 55} 56 57// Image gallery with lazy loading 58function ImageGallery({ images }) { 59 return ( 60 <div className="gallery"> 61 {images.map((image) => ( 62 <LazyImage 63 key={image.id} 64 src={image.url} 65 alt={image.alt} 66 placeholder={image.thumbnail} 67 /> 68 ))} 69 </div> 70 ); 71}

Lazy Loading Data#

1import { Suspense, use } from 'react'; 2 3// Data fetching with Suspense 4function createResource<T>(promise: Promise<T>) { 5 let status = 'pending'; 6 let result: T; 7 8 const suspender = promise.then( 9 (data) => { 10 status = 'success'; 11 result = data; 12 }, 13 (error) => { 14 status = 'error'; 15 result = error; 16 } 17 ); 18 19 return { 20 read(): T { 21 if (status === 'pending') throw suspender; 22 if (status === 'error') throw result; 23 return result; 24 }, 25 }; 26} 27 28// Usage 29const userResource = createResource(fetchUser()); 30 31function UserProfile() { 32 const user = userResource.read(); 33 return <div>{user.name}</div>; 34} 35 36function App() { 37 return ( 38 <Suspense fallback={<ProfileSkeleton />}> 39 <UserProfile /> 40 </Suspense> 41 ); 42} 43 44// React 19 use() hook 45async function fetchPosts(): Promise<Post[]> { 46 const response = await fetch('/api/posts'); 47 return response.json(); 48} 49 50function PostList({ postsPromise }: { postsPromise: Promise<Post[]> }) { 51 const posts = use(postsPromise); 52 return ( 53 <ul> 54 {posts.map(post => ( 55 <li key={post.id}>{post.title}</li> 56 ))} 57 </ul> 58 ); 59}

Skeleton Loading#

1// Skeleton components 2function SkeletonText({ width = '100%' }) { 3 return <div className="skeleton-text" style={{ width }} />; 4} 5 6function SkeletonCircle({ size = 40 }) { 7 return ( 8 <div 9 className="skeleton-circle" 10 style={{ width: size, height: size }} 11 /> 12 ); 13} 14 15function SkeletonRect({ width = '100%', height = 100 }) { 16 return ( 17 <div 18 className="skeleton-rect" 19 style={{ width, height }} 20 /> 21 ); 22} 23 24// Page skeleton 25function DashboardSkeleton() { 26 return ( 27 <div className="dashboard-skeleton"> 28 <div className="header"> 29 <SkeletonCircle size={32} /> 30 <SkeletonText width="150px" /> 31 </div> 32 <div className="cards"> 33 {[1, 2, 3].map(i => ( 34 <SkeletonRect key={i} height={200} /> 35 ))} 36 </div> 37 </div> 38 ); 39} 40 41// CSS 42const skeletonStyles = ` 43.skeleton-text, 44.skeleton-circle, 45.skeleton-rect { 46 background: linear-gradient( 47 90deg, 48 #f0f0f0 25%, 49 #e0e0e0 50%, 50 #f0f0f0 75% 51 ); 52 background-size: 200% 100%; 53 animation: shimmer 1.5s infinite; 54} 55 56@keyframes shimmer { 57 0% { background-position: 200% 0; } 58 100% { background-position: -200% 0; } 59} 60 61.skeleton-text { 62 height: 1em; 63 border-radius: 4px; 64} 65 66.skeleton-circle { 67 border-radius: 50%; 68} 69 70.skeleton-rect { 71 border-radius: 8px; 72} 73`;

Error Boundaries#

1import { Component, lazy, Suspense } from 'react'; 2 3class LazyErrorBoundary extends Component< 4 { children: React.ReactNode; fallback: React.ReactNode }, 5 { hasError: boolean } 6> { 7 state = { hasError: false }; 8 9 static getDerivedStateFromError() { 10 return { hasError: true }; 11 } 12 13 retry = () => { 14 this.setState({ hasError: false }); 15 }; 16 17 render() { 18 if (this.state.hasError) { 19 return ( 20 <div> 21 <p>Failed to load component</p> 22 <button onClick={this.retry}>Retry</button> 23 </div> 24 ); 25 } 26 return this.props.children; 27 } 28} 29 30// Usage 31const HeavyComponent = lazy(() => import('./HeavyComponent')); 32 33function App() { 34 return ( 35 <LazyErrorBoundary fallback={<ErrorMessage />}> 36 <Suspense fallback={<Loader />}> 37 <HeavyComponent /> 38 </Suspense> 39 </LazyErrorBoundary> 40 ); 41} 42 43// Retry lazy import 44function lazyWithRetry<T extends React.ComponentType<any>>( 45 factory: () => Promise<{ default: T }>, 46 retries = 3 47) { 48 return lazy(async () => { 49 let lastError: Error | undefined; 50 51 for (let i = 0; i < retries; i++) { 52 try { 53 return await factory(); 54 } catch (error) { 55 lastError = error as Error; 56 await new Promise(r => setTimeout(r, 1000 * (i + 1))); 57 } 58 } 59 60 throw lastError; 61 }); 62} 63 64const ReliableComponent = lazyWithRetry(() => import('./Component'));

Virtualized Lists#

1import { useVirtualizer } from '@tanstack/react-virtual'; 2 3function VirtualList({ items }) { 4 const parentRef = useRef<HTMLDivElement>(null); 5 6 const virtualizer = useVirtualizer({ 7 count: items.length, 8 getScrollElement: () => parentRef.current, 9 estimateSize: () => 50, 10 overscan: 5, 11 }); 12 13 return ( 14 <div ref={parentRef} className="list-container"> 15 <div 16 style={{ 17 height: `${virtualizer.getTotalSize()}px`, 18 position: 'relative', 19 }} 20 > 21 {virtualizer.getVirtualItems().map((virtualRow) => ( 22 <div 23 key={virtualRow.key} 24 style={{ 25 position: 'absolute', 26 top: 0, 27 left: 0, 28 width: '100%', 29 height: `${virtualRow.size}px`, 30 transform: `translateY(${virtualRow.start}px)`, 31 }} 32 > 33 {items[virtualRow.index].name} 34 </div> 35 ))} 36 </div> 37 </div> 38 ); 39}

Best Practices#

Component Splitting: ✓ Split by route ✓ Split heavy/rarely-used components ✓ Preload on user interaction ✓ Use meaningful chunk names Loading States: ✓ Show skeleton loaders ✓ Keep layout stable ✓ Avoid flash of loading ✓ Handle errors gracefully Images: ✓ Use native loading="lazy" ✓ Provide placeholders ✓ Use Intersection Observer ✓ Optimize image sizes Performance: ✓ Measure with DevTools ✓ Set appropriate chunk sizes ✓ Preload critical resources ✓ Test on slow networks

Conclusion#

Lazy loading is essential for fast initial page loads. Use React.lazy for components, route-based splitting for pages, and Intersection Observer for images. Combine with proper loading states and error handling for a smooth user experience.

Share this article

Help spread the word about Bootspring