How to optimize images in React

Optimizing images in React applications reduces load times and improves performance through lazy loading, responsive images, and modern formats. With over 12 years of React experience since 2014 and as the creator of CoreUI, I’ve optimized images in numerous production applications. Image optimization includes lazy loading, responsive images with srcset, modern formats like WebP and AVIF, and proper compression strategies. This approach significantly reduces bundle size and improves perceived performance with faster initial page loads.

Use lazy loading, responsive images, modern formats, and build-time optimization to reduce image load times and bandwidth.

Lazy loading images:

// LazyImage.jsx
import { useState, useEffect, useRef } from 'react'

export default function LazyImage({ src, alt, placeholder, className }) {
  const [imageSrc, setImageSrc] = useState(placeholder)
  const [isLoaded, setIsLoaded] = useState(false)
  const imgRef = useRef()

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            setImageSrc(src)
            observer.disconnect()
          }
        })
      },
      { threshold: 0.01 }
    )

    if (imgRef.current) {
      observer.observe(imgRef.current)
    }

    return () => observer.disconnect()
  }, [src])

  return (
    <img
      ref={imgRef}
      src={imageSrc}
      alt={alt}
      className={`${className} ${isLoaded ? 'loaded' : 'loading'}`}
      onLoad={() => setIsLoaded(true)}
      loading="lazy"
    />
  )
}

Responsive images with srcset:

// ResponsiveImage.jsx
export default function ResponsiveImage({ src, alt }) {
  return (
    <picture>
      {/* Modern formats first */}
      <source
        srcSet={`
          ${src}-400.avif 400w,
          ${src}-800.avif 800w,
          ${src}-1200.avif 1200w
        `}
        type="image/avif"
      />
      <source
        srcSet={`
          ${src}-400.webp 400w,
          ${src}-800.webp 800w,
          ${src}-1200.webp 1200w
        `}
        type="image/webp"
      />
      {/* Fallback */}
      <img
        src={`${src}-800.jpg`}
        srcSet={`
          ${src}-400.jpg 400w,
          ${src}-800.jpg 800w,
          ${src}-1200.jpg 1200w
        `}
        sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
        alt={alt}
        loading="lazy"
      />
    </picture>
  )
}

Vite image optimization:

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { imagetools } from 'vite-imagetools'

export default defineConfig({
  plugins: [
    react(),
    imagetools()
  ],
  build: {
    assetsInlineLimit: 4096 // Inline images < 4kb as base64
  }
})

// Usage in component
import heroImage from './hero.jpg?w=800&format=webp&quality=80'
import heroImageAvif from './hero.jpg?w=800&format=avif&quality=80'

function Hero() {
  return (
    <picture>
      <source srcSet={heroImageAvif} type="image/avif" />
      <source srcSet={heroImage} type="image/webp" />
      <img src={heroImage} alt="Hero" />
    </picture>
  )
}

Next.js Image component:

// If using Next.js
import Image from 'next/image'

export default function OptimizedImage() {
  return (
    <>
      {/* Automatic optimization, lazy loading, responsive */}
      <Image
        src="/hero.jpg"
        alt="Hero"
        width={800}
        height={600}
        quality={80}
        placeholder="blur"
        blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
        priority={false} // true for above-the-fold images
      />

      {/* Fill container */}
      <div style={{ position: 'relative', width: '100%', height: 400 }}>
        <Image
          src="/background.jpg"
          alt="Background"
          fill
          style={{ objectFit: 'cover' }}
        />
      </div>
    </>
  )
}

Progressive image loading:

// ProgressiveImage.jsx
import { useState, useEffect } from 'react'

export default function ProgressiveImage({ lowQualitySrc, highQualitySrc, alt }) {
  const [src, setSrc] = useState(lowQualitySrc)
  const [isLoading, setIsLoading] = useState(true)

  useEffect(() => {
    const img = new Image()
    img.src = highQualitySrc

    img.onload = () => {
      setSrc(highQualitySrc)
      setIsLoading(false)
    }
  }, [highQualitySrc])

  return (
    <div className="progressive-image">
      <img
        src={src}
        alt={alt}
        style={{
          filter: isLoading ? 'blur(20px)' : 'none',
          transition: 'filter 0.3s ease-out'
        }}
      />
    </div>
  )
}

Image compression script:

// scripts/optimize-images.js
import sharp from 'sharp'
import { readdirSync, statSync } from 'fs'
import { join } from 'path'

const inputDir = './public/images'
const outputDir = './public/images/optimized'

const sizes = [400, 800, 1200]
const formats = ['webp', 'avif', 'jpeg']

async function optimizeImage(inputPath, filename) {
  const name = filename.replace(/\.[^/.]+$/, '')

  for (const size of sizes) {
    for (const format of formats) {
      const output = join(outputDir, `${name}-${size}.${format}`)

      await sharp(inputPath)
        .resize(size, null, { withoutEnlargement: true })
        [format]({ quality: 80 })
        .toFile(output)

      console.log(`Created ${output}`)
    }
  }
}

async function processDirectory(dir) {
  const files = readdirSync(dir)

  for (const file of files) {
    const fullPath = join(dir, file)

    if (statSync(fullPath).isDirectory()) {
      await processDirectory(fullPath)
    } else if (/\.(jpg|jpeg|png)$/i.test(file)) {
      await optimizeImage(fullPath, file)
    }
  }
}

processDirectory(inputDir)

Custom image loader:

// ImageLoader.jsx
import { useState, useEffect } from 'react'

function useImagePreloader(imageUrls) {
  const [imagesPreloaded, setImagesPreloaded] = useState(false)

  useEffect(() => {
    const imagePromises = imageUrls.map((src) => {
      return new Promise((resolve, reject) => {
        const img = new Image()
        img.src = src
        img.onload = resolve
        img.onerror = reject
      })
    })

    Promise.all(imagePromises)
      .then(() => setImagesPreloaded(true))
      .catch(() => console.error('Failed to preload images'))
  }, [imageUrls])

  return imagesPreloaded
}

export default function Gallery({ images }) {
  const preloaded = useImagePreloader(images.map(img => img.src))

  if (!preloaded) {
    return <div>Loading images...</div>
  }

  return (
    <div className="gallery">
      {images.map((img, i) => (
        <img key={i} src={img.src} alt={img.alt} />
      ))}
    </div>
  )
}

CSS for image optimization:

/* Aspect ratio boxes to prevent layout shift */
.image-container {
  position: relative;
  width: 100%;
  padding-bottom: 56.25%; /* 16:9 aspect ratio */
  overflow: hidden;
}

.image-container img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Fade in on load */
.lazy-image {
  opacity: 0;
  transition: opacity 0.3s;
}

.lazy-image.loaded {
  opacity: 1;
}

/* Blur placeholder */
.progressive-image img {
  width: 100%;
  display: block;
}

Webpack image optimization:

// webpack.config.js
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin')

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|jpeg|gif|svg)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 4 * 1024 // 4kb
          }
        }
      }
    ]
  },
  plugins: [
    new ImageMinimizerPlugin({
      minimizer: {
        implementation: ImageMinimizerPlugin.imageminMinify,
        options: {
          plugins: [
            ['imagemin-mozjpeg', { quality: 80 }],
            ['imagemin-pngquant', { quality: [0.65, 0.9] }],
            ['imagemin-svgo']
          ]
        }
      },
      generator: [
        {
          preset: 'webp',
          implementation: ImageMinimizerPlugin.imageminGenerate,
          options: {
            plugins: ['imagemin-webp']
          }
        }
      ]
    })
  ]
}

Best Practice Note

Use native lazy loading with loading=“lazy” attribute for modern browsers. Implement intersection observer for custom lazy loading. Provide responsive images with srcset for different screen sizes. Use modern formats like WebP and AVIF with JPEG fallback. Compress images before deployment with tools like sharp or imagemin. Inline small images as base64 to reduce HTTP requests. Use aspect ratio containers to prevent layout shift. Preload critical above-the-fold images. This is how we optimize images in CoreUI React applications—lazy loading, responsive images with srcset, modern formats, and build-time compression reducing image payload by 60-80% while maintaining visual quality.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to check if a string is a number in JavaScript
How to check if a string is a number in JavaScript

How to Open Link in a New Tab in HTML?
How to Open Link in a New Tab in HTML?

JavaScript printf equivalent
JavaScript printf equivalent

Understanding and Resolving the “React Must Be in Scope When Using JSX
Understanding and Resolving the “React Must Be in Scope When Using JSX

Answers by CoreUI Core Team