Back to Blog
ReactLazy LoadingPerformanceCode Splitting

React Lazy Loading Patterns

Optimize React apps with lazy loading. From code splitting to route-based loading to image optimization.

B
Bootspring Team
Engineering
January 8, 2021
7 min read

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

React.lazy Basics#

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// Named exports 15const Dashboard = lazy(() => 16 import('./Dashboard').then(module => ({ 17 default: module.Dashboard, 18 })) 19); 20 21// With error boundary 22import { ErrorBoundary } from 'react-error-boundary'; 23 24function AppWithError() { 25 return ( 26 <ErrorBoundary fallback={<div>Something went wrong</div>}> 27 <Suspense fallback={<div>Loading...</div>}> 28 <HeavyComponent /> 29 </Suspense> 30 </ErrorBoundary> 31 ); 32}

Route-Based Code Splitting#

1import { lazy, Suspense } from 'react'; 2import { Routes, Route } from 'react-router-dom'; 3 4// Lazy load routes 5const Home = lazy(() => import('./pages/Home')); 6const Dashboard = lazy(() => import('./pages/Dashboard')); 7const Settings = lazy(() => import('./pages/Settings')); 8const Profile = lazy(() => import('./pages/Profile')); 9 10function PageLoader() { 11 return ( 12 <div className="page-loader"> 13 <Spinner /> 14 <p>Loading page...</p> 15 </div> 16 ); 17} 18 19function App() { 20 return ( 21 <Suspense fallback={<PageLoader />}> 22 <Routes> 23 <Route path="/" element={<Home />} /> 24 <Route path="/dashboard" element={<Dashboard />} /> 25 <Route path="/settings" element={<Settings />} /> 26 <Route path="/profile" element={<Profile />} /> 27 </Routes> 28 </Suspense> 29 ); 30} 31 32// With layout 33const DashboardLayout = lazy(() => import('./layouts/DashboardLayout')); 34const Analytics = lazy(() => import('./pages/Analytics')); 35const Reports = lazy(() => import('./pages/Reports')); 36 37function AppWithLayout() { 38 return ( 39 <Suspense fallback={<PageLoader />}> 40 <Routes> 41 <Route path="/dashboard" element={<DashboardLayout />}> 42 <Route index element={<Analytics />} /> 43 <Route path="reports" element={<Reports />} /> 44 </Route> 45 </Routes> 46 </Suspense> 47 ); 48}

Preloading Components#

1// Define lazy components with preload 2function lazyWithPreload<T extends React.ComponentType<any>>( 3 factory: () => Promise<{ default: T }> 4) { 5 const Component = lazy(factory); 6 (Component as any).preload = factory; 7 return Component as typeof Component & { preload: typeof factory }; 8} 9 10const Dashboard = lazyWithPreload(() => import('./Dashboard')); 11const Settings = lazyWithPreload(() => import('./Settings')); 12 13// Preload on hover 14function NavLink({ to, children, component }: { 15 to: string; 16 children: React.ReactNode; 17 component: { preload?: () => Promise<any> }; 18}) { 19 const handleMouseEnter = () => { 20 component.preload?.(); 21 }; 22 23 return ( 24 <Link to={to} onMouseEnter={handleMouseEnter}> 25 {children} 26 </Link> 27 ); 28} 29 30// Usage 31function Navigation() { 32 return ( 33 <nav> 34 <NavLink to="/dashboard" component={Dashboard}> 35 Dashboard 36 </NavLink> 37 <NavLink to="/settings" component={Settings}> 38 Settings 39 </NavLink> 40 </nav> 41 ); 42} 43 44// Preload on route change intent 45function usePreloadOnIntent(path: string, component: { preload?: () => Promise<any> }) { 46 const location = useLocation(); 47 48 useEffect(() => { 49 // Preload when user might navigate 50 if (location.pathname.startsWith('/dashboard')) { 51 component.preload?.(); 52 } 53 }, [location, component]); 54}

Component-Level Lazy Loading#

1// Lazy load modal content 2const ModalContent = lazy(() => import('./ModalContent')); 3 4function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { 5 if (!isOpen) return null; 6 7 return ( 8 <div className="modal"> 9 <Suspense fallback={<div>Loading...</div>}> 10 <ModalContent onClose={onClose} /> 11 </Suspense> 12 </div> 13 ); 14} 15 16// Lazy load tabs 17const TabAnalytics = lazy(() => import('./tabs/Analytics')); 18const TabUsers = lazy(() => import('./tabs/Users')); 19const TabSettings = lazy(() => import('./tabs/Settings')); 20 21function Tabs() { 22 const [activeTab, setActiveTab] = useState('analytics'); 23 24 const tabs = { 25 analytics: TabAnalytics, 26 users: TabUsers, 27 settings: TabSettings, 28 }; 29 30 const ActiveComponent = tabs[activeTab]; 31 32 return ( 33 <div> 34 <div className="tab-buttons"> 35 <button onClick={() => setActiveTab('analytics')}>Analytics</button> 36 <button onClick={() => setActiveTab('users')}>Users</button> 37 <button onClick={() => setActiveTab('settings')}>Settings</button> 38 </div> 39 <Suspense fallback={<TabSkeleton />}> 40 <ActiveComponent /> 41 </Suspense> 42 </div> 43 ); 44}

Image Lazy Loading#

1// Native lazy loading 2function LazyImage({ src, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) { 3 return <img src={src} alt={alt} loading="lazy" {...props} />; 4} 5 6// Intersection Observer approach 7function useIntersection( 8 ref: React.RefObject<Element>, 9 options?: IntersectionObserverInit 10) { 11 const [isIntersecting, setIsIntersecting] = useState(false); 12 13 useEffect(() => { 14 const element = ref.current; 15 if (!element) return; 16 17 const observer = new IntersectionObserver(([entry]) => { 18 setIsIntersecting(entry.isIntersecting); 19 }, options); 20 21 observer.observe(element); 22 return () => observer.disconnect(); 23 }, [ref, options]); 24 25 return isIntersecting; 26} 27 28function LazyImageWithObserver({ 29 src, 30 alt, 31 placeholder = '/placeholder.jpg', 32 ...props 33}: { 34 src: string; 35 alt: string; 36 placeholder?: string; 37} & React.ImgHTMLAttributes<HTMLImageElement>) { 38 const ref = useRef<HTMLDivElement>(null); 39 const isVisible = useIntersection(ref, { rootMargin: '100px' }); 40 const [loaded, setLoaded] = useState(false); 41 42 return ( 43 <div ref={ref} className="image-container"> 44 {!loaded && <img src={placeholder} alt="" className="placeholder" />} 45 {isVisible && ( 46 <img 47 src={src} 48 alt={alt} 49 onLoad={() => setLoaded(true)} 50 style={{ opacity: loaded ? 1 : 0 }} 51 {...props} 52 /> 53 )} 54 </div> 55 ); 56} 57 58// Progressive image loading 59function ProgressiveImage({ 60 lowQualitySrc, 61 highQualitySrc, 62 alt, 63}: { 64 lowQualitySrc: string; 65 highQualitySrc: string; 66 alt: string; 67}) { 68 const [src, setSrc] = useState(lowQualitySrc); 69 const [loaded, setLoaded] = useState(false); 70 71 useEffect(() => { 72 const img = new Image(); 73 img.src = highQualitySrc; 74 img.onload = () => { 75 setSrc(highQualitySrc); 76 setLoaded(true); 77 }; 78 }, [highQualitySrc]); 79 80 return ( 81 <img 82 src={src} 83 alt={alt} 84 className={loaded ? 'loaded' : 'loading'} 85 style={{ 86 filter: loaded ? 'none' : 'blur(10px)', 87 transition: 'filter 0.3s', 88 }} 89 /> 90 ); 91}

List Virtualization#

1import { useVirtualizer } from '@tanstack/react-virtual'; 2 3function VirtualizedList({ items }: { items: Item[] }) { 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} style={{ height: 400, overflow: 'auto' }}> 15 <div 16 style={{ 17 height: virtualizer.getTotalSize(), 18 position: 'relative', 19 }} 20 > 21 {virtualizer.getVirtualItems().map((virtualItem) => ( 22 <div 23 key={virtualItem.key} 24 style={{ 25 position: 'absolute', 26 top: 0, 27 left: 0, 28 width: '100%', 29 height: virtualItem.size, 30 transform: `translateY(${virtualItem.start}px)`, 31 }} 32 > 33 {items[virtualItem.index].name} 34 </div> 35 ))} 36 </div> 37 </div> 38 ); 39}

Conditional Loading#

1// Load based on feature flag 2function FeatureFlag({ flag, children }: { flag: string; children: React.ReactNode }) { 3 const [enabled, setEnabled] = useState(false); 4 const [Component, setComponent] = useState<React.ComponentType | null>(null); 5 6 useEffect(() => { 7 checkFeatureFlag(flag).then((enabled) => { 8 if (enabled) { 9 setEnabled(true); 10 } 11 }); 12 }, [flag]); 13 14 if (!enabled) return null; 15 16 return <>{children}</>; 17} 18 19// Load based on user role 20const AdminPanel = lazy(() => import('./AdminPanel')); 21 22function ConditionalAdmin({ user }: { user: User }) { 23 if (user.role !== 'admin') { 24 return null; 25 } 26 27 return ( 28 <Suspense fallback={<div>Loading admin panel...</div>}> 29 <AdminPanel /> 30 </Suspense> 31 ); 32} 33 34// Load based on viewport 35function useMediaQuery(query: string) { 36 const [matches, setMatches] = useState( 37 () => window.matchMedia(query).matches 38 ); 39 40 useEffect(() => { 41 const mq = window.matchMedia(query); 42 const handler = (e: MediaQueryListEvent) => setMatches(e.matches); 43 mq.addEventListener('change', handler); 44 return () => mq.removeEventListener('change', handler); 45 }, [query]); 46 47 return matches; 48} 49 50const DesktopSidebar = lazy(() => import('./DesktopSidebar')); 51 52function ResponsiveLayout() { 53 const isDesktop = useMediaQuery('(min-width: 1024px)'); 54 55 return ( 56 <div> 57 {isDesktop && ( 58 <Suspense fallback={null}> 59 <DesktopSidebar /> 60 </Suspense> 61 )} 62 <main>{/* Content */}</main> 63 </div> 64 ); 65}

Bundle Analysis#

1// Webpack magic comments for naming 2const Dashboard = lazy(() => 3 import(/* webpackChunkName: "dashboard" */ './Dashboard') 4); 5 6const Analytics = lazy(() => 7 import( 8 /* webpackChunkName: "analytics" */ 9 /* webpackPrefetch: true */ 10 './Analytics' 11 ) 12); 13 14const Reports = lazy(() => 15 import( 16 /* webpackChunkName: "reports" */ 17 /* webpackPreload: true */ 18 './Reports' 19 ) 20); 21 22// Group related components 23const AdminPages = lazy(() => 24 import(/* webpackChunkName: "admin" */ './admin') 25);

Loading States#

1// Skeleton loader 2function DashboardSkeleton() { 3 return ( 4 <div className="dashboard-skeleton"> 5 <div className="skeleton-header" /> 6 <div className="skeleton-grid"> 7 {Array.from({ length: 6 }).map((_, i) => ( 8 <div key={i} className="skeleton-card" /> 9 ))} 10 </div> 11 </div> 12 ); 13} 14 15// Deferred loading state 16function useDeferredLoading(delay = 200) { 17 const [showLoading, setShowLoading] = useState(false); 18 19 useEffect(() => { 20 const timeout = setTimeout(() => setShowLoading(true), delay); 21 return () => clearTimeout(timeout); 22 }, [delay]); 23 24 return showLoading; 25} 26 27function DeferredFallback({ delay = 200 }: { delay?: number }) { 28 const showLoading = useDeferredLoading(delay); 29 30 if (!showLoading) return null; 31 return <Spinner />; 32} 33 34// Usage 35function App() { 36 return ( 37 <Suspense fallback={<DeferredFallback />}> 38 <Dashboard /> 39 </Suspense> 40 ); 41}

Error Recovery#

1function LazyComponentWithRetry<T extends React.ComponentType<any>>( 2 factory: () => Promise<{ default: T }>, 3 retries = 3 4) { 5 return lazy(async () => { 6 let lastError: Error | null = null; 7 8 for (let i = 0; i < retries; i++) { 9 try { 10 return await factory(); 11 } catch (error) { 12 lastError = error as Error; 13 // Wait before retry 14 await new Promise(r => setTimeout(r, 1000 * (i + 1))); 15 } 16 } 17 18 throw lastError; 19 }); 20} 21 22const Dashboard = LazyComponentWithRetry(() => import('./Dashboard'));

Best Practices#

Code Splitting: ✓ Split at route level first ✓ Split large components ✓ Group related code in chunks ✓ Analyze bundle sizes Loading UX: ✓ Use meaningful loading states ✓ Show skeletons over spinners ✓ Defer loading indicator display ✓ Preload on user intent Performance: ✓ Set appropriate chunk sizes ✓ Use prefetch for likely routes ✓ Avoid over-splitting ✓ Monitor Core Web Vitals Images: ✓ Use native loading="lazy" ✓ Provide width/height ✓ Use appropriate formats ✓ Consider progressive loading

Conclusion#

Lazy loading improves initial load performance by deferring non-critical resources. Use route-based splitting for the biggest impact, preload on user intent for better UX, and always provide meaningful loading states. Monitor bundle sizes and Core Web Vitals to measure impact.

Share this article

Help spread the word about Bootspring