Back to Blog
ReactPerformanceLazy LoadingCode Splitting

Lazy Loading in React Applications

Implement lazy loading in React. From code splitting to route-based loading to image optimization.

B
Bootspring Team
Engineering
June 17, 2021
6 min read

Lazy loading improves initial load time. Here's how to implement it effectively.

React.lazy and Suspense#

1import { lazy, Suspense } from 'react'; 2 3// Lazy load component 4const HeavyComponent = lazy(() => import('./HeavyComponent')); 5 6function App() { 7 return ( 8 <Suspense fallback={<Loading />}> 9 <HeavyComponent /> 10 </Suspense> 11 ); 12} 13 14// Multiple lazy components 15const Dashboard = lazy(() => import('./Dashboard')); 16const Settings = lazy(() => import('./Settings')); 17const Profile = lazy(() => import('./Profile')); 18 19function App() { 20 const [view, setView] = useState('dashboard'); 21 22 return ( 23 <Suspense fallback={<PageSkeleton />}> 24 {view === 'dashboard' && <Dashboard />} 25 {view === 'settings' && <Settings />} 26 {view === 'profile' && <Profile />} 27 </Suspense> 28 ); 29}

Named Exports#

1// Component file exports multiple components 2// components/Charts.tsx 3export const BarChart = () => { ... }; 4export const LineChart = () => { ... }; 5export const PieChart = () => { ... }; 6 7// Lazy load specific export 8const BarChart = lazy(() => 9 import('./Charts').then(module => ({ default: module.BarChart })) 10); 11 12// Or create a helper 13function lazyNamed<T extends Record<string, React.ComponentType<any>>>( 14 factory: () => Promise<T>, 15 name: keyof T 16) { 17 return lazy(() => 18 factory().then(module => ({ default: module[name] })) 19 ); 20} 21 22const LineChart = lazyNamed( 23 () => import('./Charts'), 24 'LineChart' 25);

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 Products = lazy(() => import('./pages/Products')); 8const ProductDetail = lazy(() => import('./pages/ProductDetail')); 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="/products" element={<Products />} /> 18 <Route path="/products/:id" element={<ProductDetail />} /> 19 </Routes> 20 </Suspense> 21 </BrowserRouter> 22 ); 23} 24 25// Next.js automatic code splitting 26// pages/dashboard.tsx is automatically code split

Preloading Components#

1// Preload on hover 2const Settings = lazy(() => import('./Settings')); 3 4function NavLink() { 5 const preloadSettings = () => { 6 // Trigger dynamic import 7 import('./Settings'); 8 }; 9 10 return ( 11 <Link 12 to="/settings" 13 onMouseEnter={preloadSettings} 14 onFocus={preloadSettings} 15 > 16 Settings 17 </Link> 18 ); 19} 20 21// Preload with custom hook 22function usePreload(factory: () => Promise<any>) { 23 const preload = useCallback(() => { 24 factory(); 25 }, [factory]); 26 27 return preload; 28} 29 30// Preload after initial render 31function App() { 32 useEffect(() => { 33 // Preload components user likely needs 34 const timer = setTimeout(() => { 35 import('./Dashboard'); 36 import('./Profile'); 37 }, 2000); 38 39 return () => clearTimeout(timer); 40 }, []); 41 42 return <Home />; 43}

Error Boundaries#

1import { Component, lazy, Suspense } from 'react'; 2 3class ErrorBoundary 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 <ErrorBoundary fallback={<ErrorFallback />}> 36 <Suspense fallback={<Loading />}> 37 <HeavyComponent /> 38 </Suspense> 39 </ErrorBoundary> 40 ); 41} 42 43// Retry logic for failed imports 44function lazyWithRetry( 45 factory: () => Promise<{ default: React.ComponentType<any> }>, 46 retries = 3 47) { 48 return lazy(async () => { 49 let lastError: Error; 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'));

Lazy Loading Images#

1// Native lazy loading 2function ImageGallery({ images }: { images: string[] }) { 3 return ( 4 <div className="gallery"> 5 {images.map((src, i) => ( 6 <img 7 key={i} 8 src={src} 9 loading="lazy" 10 alt={`Image ${i + 1}`} 11 /> 12 ))} 13 </div> 14 ); 15} 16 17// Intersection Observer hook 18function useLazyImage(src: string): [string, boolean] { 19 const [imageSrc, setImageSrc] = useState(''); 20 const [isLoaded, setIsLoaded] = useState(false); 21 const imgRef = useRef<HTMLImageElement>(null); 22 23 useEffect(() => { 24 const observer = new IntersectionObserver( 25 ([entry]) => { 26 if (entry.isIntersecting) { 27 setImageSrc(src); 28 observer.disconnect(); 29 } 30 }, 31 { rootMargin: '100px' } 32 ); 33 34 if (imgRef.current) { 35 observer.observe(imgRef.current); 36 } 37 38 return () => observer.disconnect(); 39 }, [src]); 40 41 return [imageSrc, isLoaded]; 42} 43 44// Lazy image component 45function LazyImage({ src, alt, ...props }: ImgHTMLAttributes<HTMLImageElement>) { 46 const [isVisible, setIsVisible] = useState(false); 47 const ref = useRef<HTMLImageElement>(null); 48 49 useEffect(() => { 50 const observer = new IntersectionObserver( 51 ([entry]) => { 52 if (entry.isIntersecting) { 53 setIsVisible(true); 54 observer.disconnect(); 55 } 56 }, 57 { rootMargin: '50px' } 58 ); 59 60 if (ref.current) { 61 observer.observe(ref.current); 62 } 63 64 return () => observer.disconnect(); 65 }, []); 66 67 return ( 68 <img 69 ref={ref} 70 src={isVisible ? src : undefined} 71 alt={alt} 72 {...props} 73 /> 74 ); 75}

Component-Level Code Splitting#

1// Split heavy features 2const RichTextEditor = lazy(() => import('./RichTextEditor')); 3const ChartLibrary = lazy(() => import('./ChartLibrary')); 4const VideoPlayer = lazy(() => import('./VideoPlayer')); 5 6function ContentBlock({ type, data }: ContentBlockProps) { 7 return ( 8 <Suspense fallback={<BlockSkeleton type={type} />}> 9 {type === 'text' && <RichTextEditor content={data} />} 10 {type === 'chart' && <ChartLibrary data={data} />} 11 {type === 'video' && <VideoPlayer url={data.url} />} 12 </Suspense> 13 ); 14} 15 16// Modal content 17function SettingsModal({ isOpen, onClose }: ModalProps) { 18 if (!isOpen) return null; 19 20 return ( 21 <Modal onClose={onClose}> 22 <Suspense fallback={<ModalSkeleton />}> 23 <SettingsContent /> 24 </Suspense> 25 </Modal> 26 ); 27}

Progressive Loading#

1// Load content progressively 2function Dashboard() { 3 return ( 4 <div> 5 {/* Critical content first */} 6 <Header /> 7 8 {/* Secondary content lazy loaded */} 9 <Suspense fallback={<StatsSkeleton />}> 10 <Stats /> 11 </Suspense> 12 13 <Suspense fallback={<ChartsSkeleton />}> 14 <Charts /> 15 </Suspense> 16 17 <Suspense fallback={<TableSkeleton />}> 18 <DataTable /> 19 </Suspense> 20 </div> 21 ); 22} 23 24// Skeleton components 25function StatsSkeleton() { 26 return ( 27 <div className="stats-skeleton"> 28 {[1, 2, 3, 4].map(i => ( 29 <div key={i} className="stat-card skeleton" /> 30 ))} 31 </div> 32 ); 33}

Bundle Analysis#

1# Install webpack bundle analyzer 2npm install -D webpack-bundle-analyzer 3 4# For Vite 5npm install -D rollup-plugin-visualizer 6 7# Add to vite.config.ts 8import { visualizer } from 'rollup-plugin-visualizer'; 9 10export default { 11 plugins: [ 12 visualizer({ 13 open: true, 14 gzipSize: true, 15 }), 16 ], 17}; 18 19# Analyze bundle 20npm run build 21# Opens visualization in browser

Best Practices#

When to Lazy Load: ✓ Routes/pages ✓ Modals and dialogs ✓ Below-the-fold content ✓ Heavy third-party libraries ✓ Features behind feature flags Optimization: ✓ Preload on hover/focus ✓ Use meaningful loading states ✓ Implement error boundaries ✓ Analyze bundle size UX: ✓ Show skeleton loaders ✓ Avoid layout shift ✓ Preload likely next routes ✓ Progressive enhancement

Conclusion#

Lazy loading reduces initial bundle size and improves load times. Use React.lazy for components, route-based splitting for pages, and Intersection Observer for images. Combine with preloading strategies and meaningful loading states for the best user experience.

Share this article

Help spread the word about Bootspring