Back to the blog

Image Optimization: WebP, AVIF and Lazy Loading

Complete guide to image optimization: WebP and AVIF formats, lazy loading, responsive images, CDN and blur placeholder.

January 5, 2026
9 min read
634 views
MOLOTILO

MOLOTILO DIGITAL

Image Optimization: WebP, AVIF and Lazy Loading

Why image optimization matters

Images make up 50–70% of a page’s weight. Proper optimization critically affects loading speed, Core Web Vitals and the user experience.

Modern formats

FormatCompressionSupportUse
JPEGGood100%Photos
PNGLossless100%Graphics, transparency
WebPExcellent97%Universal
AVIFBest85%Photos, where supported
<!-- Fallback for older browsers -->
<picture>
  <source srcset="/image.avif" type="image/avif" />
  <source srcset="/image.webp" type="image/webp" />
  <img src="/image.jpg" alt="Description" />
</picture>

Next.js Image Component

Next.js optimizes images automatically:

import Image from 'next/image';

// Basic usage
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={600}
  priority  // For LCP images
/>

// Responsive image
<Image
  src="/product.jpg"
  alt="Product"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  className="object-cover"
/>

// With blur placeholder
import heroImage from '@/public/hero.jpg';

<Image
  src={heroImage}
  alt="Hero"
  placeholder="blur"  // Automatic blur for static imports
/>

// Dynamic blur placeholder
<Image
  src={dynamicUrl}
  alt="Dynamic"
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>

Next.js configuration

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
      }
    ],
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 days
  }
};

Lazy Loading

// Native lazy loading
<img src="/image.jpg" alt="..." loading="lazy" />

// Next.js Image — lazy by default
<Image src="/image.jpg" alt="..." width={800} height={600} />

// Disable lazy for important images
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority  // Loads immediately
  loading="eager"
/>

// Intersection Observer for custom logic
function LazyImage({ src, alt }) {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { rootMargin: '100px' }
    );

    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div ref={ref}>
      {isVisible ? (
        <img src={src} alt={alt} />
      ) : (
        <div className="skeleton" />
      )}
    </div>
  );
}

Responsive Images

<!-- srcset for different screen sizes -->
<img
  src="/image-800.jpg"
  srcset="
    /image-400.jpg 400w,
    /image-800.jpg 800w,
    /image-1200.jpg 1200w
  "
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="Responsive image"
/>

<!-- Art direction with picture -->
<picture>
  <source
    media="(max-width: 767px)"
    srcset="/hero-mobile.webp"
  />
  <source
    media="(max-width: 1023px)"
    srcset="/hero-tablet.webp"
  />
  <img src="/hero-desktop.webp" alt="Hero" />
</picture>

CDN and caching

// Cloudinary URL API
function getOptimizedUrl(publicId: string, options: ImageOptions) {
  const { width, height, quality = 'auto', format = 'auto' } = options;

  return `https://res.cloudinary.com/demo/image/upload/` +
    `w_${width},h_${height},c_fill,q_${quality},f_${format}/` +
    publicId;
}

// Usage
<img src={getOptimizedUrl('products/shoe', { width: 400, height: 400 })} />

// Imgix
const imgixUrl = `/placeholders/image-placeholder.svg?w=800&auto=format,compress`;

Generating a blur placeholder

// Generate a base64 blur on the server
import sharp from 'sharp';

async function generateBlurPlaceholder(imagePath: string): Promise<string> {
  const buffer = await sharp(imagePath)
    .resize(10, 10, { fit: 'inside' })
    .blur()
    .toBuffer();

  return `data:image/jpeg;base64,${buffer.toString('base64')}`;
}

// Usage with Prisma
const product = await prisma.product.findUnique({
  where: { id },
  select: {
    image: true,
    blurDataURL: true
  }
});

<Image
  src={product.image}
  alt="Product"
  placeholder="blur"
  blurDataURL={product.blurDataURL}
/>

Optimization checklist

  1. ✅ Use WebP/AVIF with a fallback
  2. ✅ Specify width and height (avoiding CLS)
  3. ✅ Lazy loading for below-the-fold images
  4. ✅ priority for LCP images
  5. ✅ Responsive sizes for different screens
  6. ✅ Blur placeholder for better UX
  7. ✅ CDN for fast delivery

Conclusion

Image optimization is one of the most effective ways to improve performance. Next.js Image does most of the work automatically. Use modern formats, lazy loading and a CDN for maximum speed.

Every kilobyte saved means faster loading and a better UX. Optimize images first.

Enjoyed the article?

Subscribe to our blog so you don’t miss new posts