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.