Lazy Loading: Load Resources On-Demand

Easy

Lazy loading dramatically improves initial page load by deferring non-critical resources. Learn when and how to implement lazy loading effectively.

Quick Decision Guide

Quick Implementation Guide:

Images: Use native loading="lazy" attribute on img tags. Works in all modern browsers.

React Components: Use React.lazy() with Suspense for non-critical components: const Component = lazy(() => import('./Component'))

Custom lazy loading: Use Intersection Observer API to detect when elements enter viewport, then load content.

Third-party scripts: Defer loading of chat widgets, analytics until after initial render or on user interaction.

Best Practice: Lazy load everything below the fold. Keep above-the-fold content eager-loaded for optimal LCP (Largest Contentful Paint).

Native Image Lazy Loading

HTML loading Attribute

<!-- Lazy load (loads when near viewport) -->
<img src="image.jpg" loading="lazy" alt="Description" />

<!-- Eager load (default, loads immediately) -->
<img src="hero.jpg" loading="eager" alt="Hero" />

<!-- Auto (browser decides) -->
<img src="photo.jpg" loading="auto" alt="Photo" />

How It Works

1. Browser detects images with loading="lazy"

2. Only loads images that are visible or near viewport

3. Automatically loads more as user scrolls

4. No JavaScript required!

Browser Support

✅ Chrome 77+

✅ Firefox 75+

✅ Safari 15.4+

✅ Edge 79+

React Example

function ImageGallery({ images }) {
  return (
    <div className="gallery">
      {images.map((img, index) => (
        <img
          key={img.id}
          src={img.url}
          alt={img.alt}
          loading={index < 3 ? 'eager' : 'lazy'}
          width={img.width}
          height={img.height}
        />
      ))}
    </div>
  );
}

Pro Tip: Set loading="eager" for first 2-3 images (above-the-fold), lazy for rest.

React Component Lazy Loading

React.lazy() and Suspense

import { lazy, Suspense } from 'react';

// Lazy load component
const Comments = lazy(() => import('./Comments'));
const UserProfile = lazy(() => import('./UserProfile'));

function App() {
  return (
    <div>
      <h1>My App</h1>
      
      {/* Suspense provides loading fallback */}
      <Suspense fallback={<div>Loading comments...</div>}>
        <Comments />
      </Suspense>
      
      <Suspense fallback={<ProfileSkeleton />}>
        <UserProfile />
      </Suspense>
    </div>
  );
}

Conditional Lazy Loading

function Dashboard() {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShowChart(true)}>
        Show Chart
      </button>
      
      {showChart && (
        <Suspense fallback={<ChartSkeleton />}>
          {React.createElement(lazy(() => import('./Chart')))}
        </Suspense>
      )}
    </div>
  );
}

Modal Lazy Loading

const Modal = lazy(() => import('./Modal'));

function App() {
  const [isOpen, setIsOpen] = useState(false);
  
  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open Modal</button>
      
      {isOpen && (
        <Suspense fallback={null}>
          <Modal onClose={() => setIsOpen(false)} />
        </Suspense>
      )}
    </>
  );
}

Intersection Observer

Custom Lazy Loading

function lazyLoad(target) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // Element is visible
        const img = entry.target;
        img.src = img.dataset.src; // Load actual image
        observer.unobserve(img); // Stop observing
      }
    });
  }, {
    rootMargin: '50px' // Start loading 50px before visible
  });
  
  observer.observe(target);
}

// Usage
document.querySelectorAll('img[data-src]').forEach(lazyLoad);

HTML Setup

<!-- Placeholder in src, actual image in data-src -->
<img 
  src="placeholder.jpg" 
  data-src="actual-image.jpg" 
  alt="Description"
/>

React Hook

function useIntersectionObserver(ref, callback, options) {
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            callback();
          }
        });
      },
      options
    );
    
    if (ref.current) {
      observer.observe(ref.current);
    }
    
    return () => observer.disconnect();
  }, [ref, callback, options]);
}

// Usage
function LazyComponent() {
  const ref = useRef(null);
  const [isVisible, setIsVisible] = useState(false);
  
  useIntersectionObserver(ref, () => setIsVisible(true), {
    rootMargin: '100px'
  });
  
  return (
    <div ref={ref}>
      {isVisible ? <HeavyContent /> : <Placeholder />}
    </div>
  );
}

Lazy Loading Third-Party Scripts

Defer Analytics

// Load Google Analytics after page interactive
window.addEventListener('load', () => {
  const script = document.createElement('script');
  script.src = 'https://www.googletagmanager.com/gtag/js?id=GA_ID';
  script.async = true;
  document.head.appendChild(script);
});

Load on Interaction

// Load chat widget on user interaction
let chatLoaded = false;

function loadChatWidget() {
  if (chatLoaded) return;
  
  const script = document.createElement('script');
  script.src = 'https://cdn.chat-widget.com/widget.js';
  script.async = true;
  document.body.appendChild(script);
  chatLoaded = true;
}

// Load on first interaction
['mousemove', 'scroll', 'keydown', 'click'].forEach(event => {
  document.addEventListener(event, loadChatWidget, { once: true });
});

Next.js Script Component

import Script from 'next/script';

function App() {
  return (
    <>
      {/* Load after page is interactive */}
      <Script
        src="https://analytics.com/script.js"
        strategy="lazyOnload"
      />
      
      {/* Load after hydration */}
      <Script
        src="https://maps.googleapis.com/maps/api/js"
        strategy="afterInteractive"
      />
    </>
  );
}

Lazy Loading Strategies

When to Lazy Load

✅ Good Candidates

Below-the-fold images: Not visible on initial load
Modal dialogs: Only needed when user opens
Admin panels: Rarely accessed features
Comments section: Secondary content
Chat widgets: Third-party scripts
Analytics: Non-critical tracking

❌ Don't Lazy Load

Hero images: Above-the-fold content
Navigation: Critical UI elements
First screen content: Impacts LCP
Small resources: Overhead not worth it
Critical user paths: Login, checkout

Loading Strategies

1. On Visibility

// Load when element enters viewport
<IntersectionObserver>
  <LazyComponent />
</IntersectionObserver>

2. On Interaction

// Load when user clicks
<button onClick={() => import('./Feature')}>
  Load Feature
</button>

3. On Idle

// Load during browser idle time
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    import('./analytics');
  });
}

4. On Route

// Load when navigating to route
const Dashboard = lazy(() => import('./Dashboard'));

Performance Impact

Before lazy loading:
  Initial load: 2.5MB, 45 requests, 4.2s load time
  
After lazy loading:
  Initial load: 800KB, 12 requests, 1.3s load time
  
Improvement: 68% smaller, 73% fewer requests, 69% faster

Best Practices

Image Lazy Loading

function OptimizedImage({ src, alt, priority = false }) {
  return (
    <img
      src={src}
      alt={alt}
      loading={priority ? 'eager' : 'lazy'}
      decoding="async"
      // Always specify dimensions to avoid layout shift
      width="800"
      height="600"
    />
  );
}

Component Lazy Loading

// ✅ Good: Lazy load heavy components
const Chart = lazy(() => import('./Chart'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));

// ❌ Bad: Lazy loading tiny components
const Button = lazy(() => import('./Button')); // Overhead > benefit

Error Handling

import { ErrorBoundary } from 'react-error-boundary';

function App() {
  return (
    <ErrorBoundary fallback={<ErrorMessage />}>
      <Suspense fallback={<Loading />}>
        <LazyComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

Prefetching

// Prefetch on hover for instant loading
function NavigationLink({ to }) {
  const handleMouseEnter = () => {
    import('./pages/' + to); // Prefetch component
  };
  
  return (
    <Link to={to} onMouseEnter={handleMouseEnter}>
      {to}
    </Link>
  );
}

Progressive Enhancement

<!-- Works without JavaScript -->
<img src="image.jpg" loading="lazy" alt="Description" />

<!-- Fallback for older browsers -->
<noscript>
  <img src="image.jpg" alt="Description" />
</noscript>