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.