Speeding Up Image Loading and Responsiveness in React

October 14, 2024 (1y ago)

Modern web development requires responsive images across various devices and screen sizes. Loading unnecessarily large images for smaller screens wastes bandwidth and slows performance. This article explores three practical techniques to optimize image loading in React applications.

1. Native Lazy Loading

The simplest optimization is the browser-native loading="lazy" attribute. It defers image loading until the user scrolls near them, preventing all images from loading simultaneously during the initial page load.

export default function BlogPost() {
  return (
    <div>
      <h1>My Blog Post</h1>
      <img
        src="/images/hero.webp"
        alt="Hero image"
        loading="lazy"
        width={800}
        height={400}
      />
      <p>Content here...</p>
      <img
        src="/images/content.webp"
        alt="Content image"
        loading="lazy"
        width={600}
        height={300}
      />
    </div>
  );
}

This allows non-critical resources to load only when needed, reducing network strain and improving perceived performance — especially on pages with many images.

2. Placeholder Images During Loading

Displaying a low-resolution placeholder while the full image loads asynchronously creates a smoother visual experience, particularly on slower connections.

import { useState } from "react";
 
export default function ProgressiveImage({ src, placeholder, alt, ...props }) {
  const [loaded, setLoaded] = useState(false);
 
  return (
    <div style={{ position: "relative" }}>
      {/* Low-res placeholder */}
      <img
        src={placeholder}
        alt={alt}
        style={{
          position: "absolute",
          inset: 0,
          opacity: loaded ? 0 : 1,
          transition: "opacity 0.3s ease",
          filter: "blur(8px)",
        }}
        {...props}
      />
      {/* Full-resolution image */}
      <img
        src={src}
        alt={alt}
        onLoad={() => setLoaded(true)}
        style={{
          opacity: loaded ? 1 : 0,
          transition: "opacity 0.3s ease",
        }}
        {...props}
      />
    </div>
  );
}

Usage:

<ProgressiveImage
  src="/images/full-size.webp"
  placeholder="/images/tiny-placeholder.webp"
  alt="Product photo"
  width={800}
  height={400}
/>

Once the full image loads, it replaces the blurred placeholder with a smooth fade transition.

3. Adaptive Image Loading

Different screen sizes require different image resolutions. Using the <picture> element with <source> tags and media / srcset attributes lets you serve the right image for the right viewport.

export default function AdaptiveImage({ alt, basePath }) {
  return (
    <picture>
      {/* Extra large screens > 1200px */}
      <source
        media="(min-width: 1200px)"
        srcSet={`${basePath}-xl.webp`}
      />
      {/* Large screens 1000px–1200px */}
      <source
        media="(min-width: 1000px)"
        srcSet={`${basePath}-lg.webp`}
      />
      {/* Medium screens 500px–1000px */}
      <source
        media="(min-width: 500px)"
        srcSet={`${basePath}-md.webp`}
      />
      {/* Small screens < 500px */}
      <img
        src={`${basePath}-sm.webp`}
        alt={alt}
        loading="lazy"
        style={{ width: "100%", height: "auto" }}
      />
    </picture>
  );
}

Usage:

<AdaptiveImage
  basePath="/images/product"
  alt="Product photo"
/>

The browser picks the first matching <source> and only downloads that variant — a 400px mobile screen never fetches a 2400px desktop image.

Combining All Three

You can combine all three techniques into a single reusable component:

import { useState } from "react";
 
export default function OptimizedImage({ basePath, placeholder, alt }) {
  const [loaded, setLoaded] = useState(false);
 
  return (
    <div style={{ position: "relative" }}>
      {!loaded && (
        <img
          src={placeholder}
          alt={alt}
          style={{ width: "100%", filter: "blur(8px)" }}
        />
      )}
      <picture style={{ opacity: loaded ? 1 : 0, transition: "opacity 0.3s" }}>
        <source media="(min-width: 1200px)" srcSet={`${basePath}-xl.webp`} />
        <source media="(min-width: 1000px)" srcSet={`${basePath}-lg.webp`} />
        <source media="(min-width: 500px)"  srcSet={`${basePath}-md.webp`} />
        <img
          src={`${basePath}-sm.webp`}
          alt={alt}
          loading="lazy"
          onLoad={() => setLoaded(true)}
          style={{ width: "100%", height: "auto" }}
        />
      </picture>
    </div>
  );
}

Conclusion

Implementing these three techniques — lazy loading, placeholder images, and adaptive variants — significantly improves React application performance and user experience across all devices. They are low-effort changes that directly impact Core Web Vitals scores, particularly LCP (Largest Contentful Paint) and CLS (Cumulative Layout Shift).

React
Performance
Web Vitals
Image Optimization

Author

Shailesh Jadav