We use cookies to enhance your experience on the site
CodeWorlds

Lazy loading - images, routes, components

Lazy loading is an optimization strategy that involves delaying the loading of resources until they actually become needed. Instead of loading everything at once during the first page render, we load elements "on demand" - when the user scrolls the page, navigates to a new route, or performs an action requiring a specific component.

Benefits of lazy loading

1. Faster initial load

  • Reduced initial bundle size
  • Faster first contentful paint (FCP)
  • Better perceived performance by users

2. Resource savings

  • Lower RAM usage
  • Reduced data transfer
  • Battery savings on mobile devices

3. Better user experience

  • Smooth scrolling without blocking
  • Progressive content loading
  • Adaptation to device capabilities and connection

Lazy loading images

Native lazy loading in HTML

1<!-- Native lazy loading (supported by modern browsers) -->
2<img src="hero-image.jpg" alt="Main image" loading="lazy" />
3
4<!-- Eager loading for above-the-fold images -->
5<img src="logo.jpg" alt="Logo" loading="eager" />
6
7<!-- Auto (default browser behavior) -->
8<img src="banner.jpg" alt="Banner" loading="auto" />

Intersection Observer API

1// Lazy loading implementation with Intersection Observer
2class LazyImageLoader {
3  constructor() {
4    this.imageObserver = new IntersectionObserver(
5      this.handleIntersection.bind(this),
6      {
7        root: null, // viewport
8        rootMargin: '50px', // load 50px before entering viewport
9        threshold: 0.1 // trigger when 10% of image is visible
10      }
11    );
12    
13    this.initLazyImages();
14  }
15  
16  initLazyImages() {
17    const lazyImages = document.querySelectorAll('img[data-src]');
18    lazyImages.forEach(img => {
19      this.imageObserver.observe(img);
20    });
21  }
22  
23  handleIntersection(entries) {
24    entries.forEach(entry => {
25      if (entry.isIntersecting) {
26        const img = entry.target;
27        this.loadImage(img);
28        this.imageObserver.unobserve(img);
29      }
30    });
31  }
32  
33  loadImage(img) {
34    // Load image from the data-src attribute
35    img.src = img.dataset.src;
36    img.classList.add('loaded');
37
38    // Remove data-src after loading
39    delete img.dataset.src;
40
41    // Error handling for loading
42    img.onerror = () => {
43      img.src = '/images/error-placeholder.png';
44      img.classList.add('error');
45    };
46  }
47}
48
49// Initialization
50document.addEventListener('DOMContentLoaded', () => {
51  new LazyImageLoader();
52});

React components for lazy loading images

1// Hook for lazy loading images
2import { useState, useRef, useEffect } from 'react';
3
4function useIntersectionObserver(options = {}) {
5  const [isIntersecting, setIsIntersecting] = useState(false);
6  const [hasIntersected, setHasIntersected] = useState(false);
7  const elementRef = useRef(null);
8  
9  useEffect(() => {
10    const element = elementRef.current;
11    if (!element) return;
12    
13    const observer = new IntersectionObserver(
14      ([entry]) => {
15        const isElementIntersecting = entry.isIntersecting;
16        setIsIntersecting(isElementIntersecting);
17        
18        if (isElementIntersecting && !hasIntersected) {
19          setHasIntersected(true);
20        }
21      },
22      { threshold: 0.1, rootMargin: '50px', ...options }
23    );
24    
25    observer.observe(element);
26    
27    return () => observer.disconnect();
28  }, [hasIntersected, options]);
29  
30  return { elementRef, isIntersecting, hasIntersected };
31}
32
33// LazyImage component
34function LazyImage({ src, alt, placeholder, className = '' }) {
35  const [imageLoaded, setImageLoaded] = useState(false);
36  const [imageError, setImageError] = useState(false);
37  const { elementRef, hasIntersected } = useIntersectionObserver();
38
39  const handleImageLoad = () => {
40    setImageLoaded(true);
41  };
42
43  const handleImageError = () => {
44    setImageError(true);
45  };
46
47  return (
48    <div ref={elementRef} className={`lazy-image-container ${className}`}>
49      {/* Placeholder shown during loading */}
50      {!imageLoaded && (
51        <img
52          src={placeholder}
53          alt={`${alt} placeholder`}
54          className="placeholder-image"
55        />
56      )}
57
58      {/* Actual image loaded lazily */}
59      {hasIntersected && (
60        <img
61          src={imageError ? '/images/error.png' : src}
62          alt={alt}
63          onLoad={handleImageLoad}
64          onError={handleImageError}
65          className={`lazy-image ${imageLoaded ? 'loaded' : 'loading'}`}
66        />
67      )}
68      
69      {/* Loading spinner */}
70      {hasIntersected && !imageLoaded && !imageError && (
71        <div className="image-spinner">Loading...</div>
72      )}
73    </div>
74  );
75}
76
77// Component usage
78function Gallery({ images }) {
79  return (
80    <div className="gallery">
81      {images.map((image, index) => (
82        <LazyImage
83          key={index}
84          src={image.url}
85          alt={image.alt}
86          placeholder={image.placeholder}
87          className="gallery-item"
88        />
89      ))}
90    </div>
91  );
92}

Progressive Image Loading

1// Component with progressive loading (blur -> sharp)
2function ProgressiveImage({ src, placeholder, alt }) {
3  const [imageLoaded, setImageLoaded] = useState(false);
4  const { elementRef, hasIntersected } = useIntersectionObserver();
5
6  return (
7    <div ref={elementRef} className="progressive-image">
8      {/* Blurred placeholder */}
9      <img
10        src={placeholder}
11        alt={alt}
12        className={`progressive-placeholder`}
13        style={{
14          filter: 'blur(10px)',
15          transition: 'opacity 0.3s ease'
16        }}
17      />
18      
19      {/* Sharp image */}
20      {hasIntersected && (
21        <img
22          src={src}
23          alt={alt}
24          onLoad={() => setImageLoaded(true)}
25          className={`main-image ${imageLoaded ? 'visible' : 'hidden'}`}
26          style={{
27            opacity: imageLoaded ? 1 : 0,
28            transition: 'opacity 0.3s ease'
29          }}
30        />
31      )}
32    </div>
33  );
34}

Lazy loading routes (Route-based)

React Router with Suspense

1import { Suspense, lazy } from 'react';
2import { Routes, Route } from 'react-router-dom';
3import ErrorBoundary from './components/ErrorBoundary';
4import LoadingSpinner from './components/LoadingSpinner';
5
6// Lazy loading route components
7const HomePage = lazy(() => import('./pages/HomePage'));
8const ProductsPage = lazy(() => import('./pages/ProductsPage'));
9const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage'));
10const UserProfilePage = lazy(() => import('./pages/UserProfilePage'));
11const AdminPanel = lazy(() => import('./pages/AdminPanel'));
12
13// Components with custom loading and error handling
14const LazyRoute = ({ children }) => (
15  <ErrorBoundary>
16    <Suspense fallback={<LoadingSpinner />}>
17      {children}
18    </Suspense>
19  </ErrorBoundary>
20);
21
22function App() {
23  return (
24    <Routes>
25      <Route path="/" element={<LazyRoute><HomePage /></LazyRoute>} />
26      <Route path="/products" element={<LazyRoute><ProductsPage /></LazyRoute>} />
27      <Route path="/products/:id" element={<LazyRoute><ProductDetailPage /></LazyRoute>} />
28      <Route path="/profile" element={<LazyRoute><UserProfilePage /></LazyRoute>} />
29      <Route path="/admin/*" element={<LazyRoute><AdminPanel /></LazyRoute>} />
30    </Routes>
31  );
32}

Advanced route loading strategies

1// Preloading routes based on user behavior
2class RoutePreloader {
3  constructor() {
4    this.preloadedRoutes = new Set();
5    this.setupPreloading();
6  }
7
8  setupPreloading() {
9    // Preload on hover
10    this.setupHoverPreloading();
11
12    // Preload based on probability
13    this.setupPredictivePreloading();
14
15    // Preload in idle time
16    this.setupIdlePreloading();
17  }
18
19  setupHoverPreloading() {
20    document.addEventListener('mouseover', (e) => {
21      const link = e.target.closest('a[href]');
22      if (link && this.shouldPreload(link.href)) {
23        this.preloadRoute(link.href);
24      }
25    });
26  }
27
28  setupPredictivePreloading() {
29    // If user is on /products, preload /products/:id
30    const currentPath = window.location.pathname;
31
32    if (currentPath === '/products') {
33      // Delay so as not to affect current page load
34      setTimeout(() => {
35        this.preloadRoute('/products/popular');
36      }, 2000);
37    }
38  }
39
40  setupIdlePreloading() {
41    if ('requestIdleCallback' in window) {
42      requestIdleCallback(() => {
43        this.preloadCriticalRoutes();
44      });
45    }
46  }
47
48  preloadRoute(path) {
49    if (this.preloadedRoutes.has(path)) return;
50
51    const routeMap = {
52      '/products': () => import('./pages/ProductsPage'),
53      '/profile': () => import('./pages/UserProfilePage'),
54      '/admin': () => import('./pages/AdminPanel')
55    };
56
57    const preloadFunction = routeMap[path];
58    if (preloadFunction) {
59      preloadFunction().then(() => {
60        this.preloadedRoutes.add(path);
61        console.log(`Route ${path} preloaded`);
62      });
63    }
64  }
65
66  shouldPreload(href) {
67    // Preload only internal links
68    return href.startsWith(window.location.origin);
69  }
70
71  preloadCriticalRoutes() {
72    const critical = ['/products', '/profile'];
73    critical.forEach(route => this.preloadRoute(route));
74  }
75}
76
77// Initialization
78new RoutePreloader();

Lazy loading components

Component-level lazy loading

1// Lazy loading expensive components
2const ExpensiveChart = lazy(() => import('./components/ExpensiveChart'));
3const DataVisualization = lazy(() => import('./components/DataVisualization'));
4const VideoPlayer = lazy(() => import('./components/VideoPlayer'));
5const CodeEditor = lazy(() => import('./components/CodeEditor'));
6
7function Dashboard({ activeTab, data }) {
8  return (
9    <div className="dashboard">
10      <nav className="dashboard-nav">
11        {/* Navigation always visible */}
12      </nav>
13
14      <main className="dashboard-content">
15        {activeTab === 'charts' && (
16          <Suspense fallback={<div>Loading charts...</div>}>
17            <ExpensiveChart data={data} />
18          </Suspense>
19        )}
20
21        {activeTab === 'visualization' && (
22          <Suspense fallback={<div>Loading visualization...</div>}>
23            <DataVisualization data={data} />
24          </Suspense>
25        )}
26
27        {activeTab === 'video' && (
28          <Suspense fallback={<div>Loading player...</div>}>
29            <VideoPlayer />
30          </Suspense>
31        )}
32
33        {activeTab === 'editor' && (
34          <Suspense fallback={<div>Loading editor...</div>}>
35            <CodeEditor />
36          </Suspense>
37        )}
38      </main>
39    </div>
40  );
41}

Modal and drawer lazy loading

1// Hook for lazy modals
2function useLazyModal() {
3  const [isOpen, setIsOpen] = useState(false);
4  const [Component, setComponent] = useState(null);
5
6  const openModal = async (modalType) => {
7    const modalMap = {
8      'user-settings': () => import('./modals/UserSettingsModal'),
9      'payment': () => import('./modals/PaymentModal'),
10      'image-editor': () => import('./modals/ImageEditorModal'),
11    };
12
13    const importFunction = modalMap[modalType];
14    if (importFunction) {
15      const { default: ModalComponent } = await importFunction();
16      setComponent(() => ModalComponent);
17      setIsOpen(true);
18    }
19  };
20
21  const closeModal = () => {
22    setIsOpen(false);
23    // Optionally: unload component after closing
24    setTimeout(() => setComponent(null), 300);
25  };
26
27  return { Component, isOpen, openModal, closeModal };
28}
29
30// Using lazy modals
31function App() {
32  const { Component: ModalComponent, isOpen, openModal, closeModal } = useLazyModal();
33
34  return (
35    <div>
36      <button onClick={() => openModal('user-settings')}>
37        User Settings
38      </button>
39      <button onClick={() => openModal('payment')}>
40        Payment
41      </button>
42      <button onClick={() => openModal('image-editor')}>
43        Image Editor
44      </button>
45
46      {isOpen && ModalComponent && (
47        <Suspense fallback={<div>Loading...</div>}>
48          <ModalComponent onClose={closeModal} />
49        </Suspense>
50      )}
51    </div>
52  );
53}

Feature-based lazy loading

1// Lazy loading entire features
2const AdminFeature = lazy(() => import('./features/Admin'));
3const AnalyticsFeature = lazy(() => import('./features/Analytics'));
4const ShoppingCartFeature = lazy(() => import('./features/ShoppingCart'));
5
6function FeatureLoader({ feature, ...props }) {
7  const featureMap = {
8    admin: AdminFeature,
9    analytics: AnalyticsFeature,
10    cart: ShoppingCartFeature
11  };
12
13  const FeatureComponent = featureMap[feature];
14
15  if (!FeatureComponent) {
16    return <div>Unknown feature: {feature}</div>;
17  }
18
19  return (
20    <ErrorBoundary>
21      <Suspense fallback={<FeatureLoadingSkeleton feature={feature} />}>
22        <FeatureComponent {...props} />
23      </Suspense>
24    </ErrorBoundary>
25  );
26}
27
28// Skeleton for different features
29function FeatureLoadingSkeleton({ feature }) {
30  const skeletons = {
31    admin: <AdminSkeleton />,
32    analytics: <ChartsSkeleton />,
33    cart: <CartSkeleton />
34  };
35
36  return skeletons[feature] || <DefaultSkeleton />;
37}

Advanced lazy loading techniques

Virtual scrolling with lazy loading

1// Virtual scrolling for large lists
2import { FixedSizeList as List } from 'react-window';
3
4function VirtualizedList({ items, itemHeight = 50 }) {
5  const [loadedItems, setLoadedItems] = useState(new Set());
6
7  const loadItem = useCallback(async (index) => {
8    if (loadedItems.has(index)) return;
9
10    // Simulate data loading
11    await new Promise(resolve => setTimeout(resolve, 100));
12    setLoadedItems(prev => new Set([...prev, index]));
13  }, [loadedItems]);
14
15  const Row = ({ index, style }) => {
16    const item = items[index];
17    const isLoaded = loadedItems.has(index);
18
19    useEffect(() => {
20      if (!isLoaded) {
21        loadItem(index);
22      }
23    }, [index, isLoaded, loadItem]);
24
25    return (
26      <div style={style} className="list-item">
27        {isLoaded ? (
28          <ItemContent item={item} />
29        ) : (
30          <ItemSkeleton />
31        )}
32      </div>
33    );
34  };
35
36  return (
37    <List
38      height={400}
39      itemCount={items.length}
40      itemSize={itemHeight}
41      overscanCount={5} // Preload 5 items beyond viewport
42    >
43      {Row}
44    </List>
45  );
46}

Lazy loading with cache

1// Cache for lazy-loaded components
2class ComponentCache {
3  constructor() {
4    this.cache = new Map();
5  }
6
7  async get(key, loader) {
8    if (this.cache.has(key)) {
9      return this.cache.get(key);
10    }
11
12    const component = await loader();
13    this.cache.set(key, component);
14    return component;
15  }
16
17  preload(key, loader) {
18    if (!this.cache.has(key)) {
19      this.get(key, loader);
20    }
21  }
22
23  clear(key) {
24    if (key) {
25      this.cache.delete(key);
26    } else {
27      this.cache.clear();
28    }
29  }
30}
31
32const componentCache = new ComponentCache();
33
34// Hook utilizing cache
35function useLazyComponent(componentKey, loader) {
36  const [Component, setComponent] = useState(null);
37  const [loading, setLoading] = useState(false);
38  const [error, setError] = useState(null);
39
40  const loadComponent = useCallback(async () => {
41    if (Component) return;
42
43    setLoading(true);
44    setError(null);
45
46    try {
47      const loadedComponent = await componentCache.get(componentKey, loader);
48      setComponent(() => loadedComponent.default);
49    } catch (err) {
50      setError(err);
51    } finally {
52      setLoading(false);
53    }
54  }, [componentKey, loader, Component]);
55
56  // Preload function
57  const preload = useCallback(() => {
58    componentCache.preload(componentKey, loader);
59  }, [componentKey, loader]);
60
61  return { Component, loading, error, loadComponent, preload };
62}

Connection-aware lazy loading

1// Adaptive loading based on connection
2function useConnectionAwareLazyLoading() {
3  const [connectionType, setConnectionType] = useState('4g');
4
5  useEffect(() => {
6    const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
7
8    if (connection) {
9      setConnectionType(connection.effectiveType);
10
11      const updateConnection = () => {
12        setConnectionType(connection.effectiveType);
13      };
14
15      connection.addEventListener('change', updateConnection);
16      return () => connection.removeEventListener('change', updateConnection);
17    }
18  }, []);
19
20  const shouldLazyLoad = useCallback((priority = 'normal') => {
21    const strategies = {
22      '4g': { immediate: true, normal: true, low: true },
23      '3g': { immediate: true, normal: true, low: false },
24      '2g': { immediate: true, normal: false, low: false },
25      'slow-2g': { immediate: false, normal: false, low: false }
26    };
27
28    return strategies[connectionType]?.[priority] ?? false;
29  }, [connectionType]);
30
31  const getLoadingStrategy = useCallback(() => {
32    const strategies = {
33      '4g': 'aggressive', // Load everything
34      '3g': 'moderate',   // Load only when needed
35      '2g': 'conservative', // Load minimally
36      'slow-2g': 'minimal' // Only essentials
37    };
38
39    return strategies[connectionType] || 'moderate';
40  }, [connectionType]);
41
42  return { connectionType, shouldLazyLoad, getLoadingStrategy };
43}
44
45// Component adapting to connection
46function AdaptiveImageGrid({ images }) {
47  const { shouldLazyLoad, getLoadingStrategy } = useConnectionAwareLazyLoading();
48  const strategy = getLoadingStrategy();
49
50  const imageSettings = {
51    aggressive: { quality: 'high', eager: 10 },
52    moderate: { quality: 'medium', eager: 4 },
53    conservative: { quality: 'low', eager: 2 },
54    minimal: { quality: 'thumbnail', eager: 1 }
55  };
56
57  const settings = imageSettings[strategy];
58
59  return (
60    <div className="image-grid">
61      {images.map((image, index) => (
62        <AdaptiveImage
63          key={image.id}
64          src={image.url}
65          quality={settings.quality}
66          placeholder={image.placeholder}
67          loading={index < settings.eager ? 'eager' : 'lazy'}
68          shouldOptimize={strategy !== 'aggressive'}
69        />
70      ))}
71    </div>
72  );
73}

Performance monitoring for lazy loading

1// Lazy loading performance monitoring
2class LazyLoadingMonitor {
3  constructor() {
4    this.metrics = {
5      componentsLoaded: 0,
6      totalLoadTime: 0,
7      failedLoads: 0,
8      cacheHits: 0
9    };
10
11    this.startTime = performance.now();
12  }
13
14  trackComponentLoad(componentName, loadTime, fromCache = false) {
15    this.metrics.componentsLoaded++;
16    this.metrics.totalLoadTime += loadTime;
17
18    if (fromCache) {
19      this.metrics.cacheHits++;
20    }
21
22    // Web Vitals tracking
23    if (loadTime > 2500) { // Slow loading threshold
24      console.warn(`Slow lazy load detected: ${componentName} took ${loadTime}ms`);
25    }
26
27    // Send to analytics
28    this.sendMetrics('component_lazy_load', {
29      component: componentName,
30      loadTime,
31      fromCache,
32      timestamp: Date.now()
33    });
34  }
35
36  trackFailedLoad(componentName, error) {
37    this.metrics.failedLoads++;
38
39    console.error(`Failed to lazy load ${componentName}:`, error);
40
41    this.sendMetrics('component_lazy_load_error', {
42      component: componentName,
43      error: error.message,
44      timestamp: Date.now()
45    });
46  }
47
48  getAverageLoadTime() {
49    return this.metrics.componentsLoaded > 0
50      ? this.metrics.totalLoadTime / this.metrics.componentsLoaded
51      : 0;
52  }
53
54  getCacheHitRate() {
55    return this.metrics.componentsLoaded > 0
56      ? (this.metrics.cacheHits / this.metrics.componentsLoaded) * 100
57      : 0;
58  }
59
60  sendMetrics(event, data) {
61    // Integration with Google Analytics, Mixpanel, etc.
62    if (typeof gtag !== 'undefined') {
63      gtag('event', event, data);
64    }
65  }
66
67  generateReport() {
68    const sessionTime = performance.now() - this.startTime;
69
70    return {
71      sessionDuration: sessionTime,
72      componentsLoaded: this.metrics.componentsLoaded,
73      averageLoadTime: this.getAverageLoadTime(),
74      cacheHitRate: this.getCacheHitRate(),
75      failureRate: (this.metrics.failedLoads / this.metrics.componentsLoaded) * 100,
76      totalFailures: this.metrics.failedLoads
77    };
78  }
79}
80
81// Singleton instance
82const lazyLoadMonitor = new LazyLoadingMonitor();
83
84// Hook with monitoring
85function useMonitoredLazyComponent(componentName, loader) {
86  const [Component, setComponent] = useState(null);
87  const [loading, setLoading] = useState(false);
88
89  const loadComponent = useCallback(async () => {
90    if (Component) return;
91
92    const startTime = performance.now();
93    setLoading(true);
94
95    try {
96      const loadedComponent = await loader();
97      const loadTime = performance.now() - startTime;
98
99      setComponent(() => loadedComponent.default);
100      lazyLoadMonitor.trackComponentLoad(componentName, loadTime);
101    } catch (error) {
102      lazyLoadMonitor.trackFailedLoad(componentName, error);
103      throw error;
104    } finally {
105      setLoading(false);
106    }
107  }, [componentName, loader, Component]);
108
109  return { Component, loading, loadComponent };
110}

Best practices for lazy loading

1. Content prioritization

1// Above-the-fold content - eager loading
2// Below-the-fold content - lazy loading
3function ContentStrategy() {
4  return (
5    <>
6      {/* Critical path - eager */}
7      <Header />
8      <Hero />
9      <MainContent />
10
11      {/* Secondary content - lazy */}
12      <Suspense fallback={<SectionSkeleton />}>
13        <LazySection name="testimonials" />
14      </Suspense>
15
16      <Suspense fallback={<SectionSkeleton />}>
17        <LazySection name="newsletter" />
18      </Suspense>
19
20      {/* Low priority - very lazy */}
21      <Suspense fallback={<FooterSkeleton />}>
22        <LazyFooter />
23      </Suspense>
24    </>
25  );
26}

2. Graceful fallbacks

1// Always provide graceful fallbacks
2function LazyComponentWrapper({ children, fallback, errorFallback }) {
3  return (
4    <ErrorBoundary
5      fallback={errorFallback || <div>An error occurred during loading</div>}
6    >
7      <Suspense fallback={fallback || <LoadingSkeleton />}>
8        {children}
9      </Suspense>
10    </ErrorBoundary>
11  );
12}

3. Strategic preloading

1// Strategic preloading based on user behavior
2function useStrategicPreloading() {
3  useEffect(() => {
4    // Preload after 2 seconds of inactivity
5    const timeoutId = setTimeout(() => {
6      // Preload likely next components
7      import('./components/UserProfile');
8      import('./components/ShoppingCart');
9    }, 2000);
10
11    // Preload based on mouse movement toward a link
12    const handleMouseMove = (e) => {
13      // If cursor is approaching a specific element
14      // preload the associated component
15    };
16
17    document.addEventListener('mousemove', handleMouseMove);
18
19    return () => {
20      clearTimeout(timeoutId);
21      document.removeEventListener('mousemove', handleMouseMove);
22    };
23  }, []);
24}

Summary

Lazy loading is a powerful optimization technique that:

  1. Reduces initial bundle size - faster application startup
  2. Improves Core Web Vitals - better FCP, LCP, CLS
  3. Saves resources - memory, transfer, battery
  4. Scales with the application - automatic optimization when adding features
  5. Adapts to conditions - responsive loading based on connection

The key to success is balancing performance with user experience - lazy loading should be invisible to the user while significantly improving application performance.

Go to CodeWorlds