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 splitPreloading 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 browserBest 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.