The Complete Guide to Optimizing Images for the Web

How to reduce image sizes by up to 90% without visible quality loss — covering modern formats, responsive loading, and automated pipelines.

Why Image Optimization Matters

Images account for approximately 50% of the average web page's total weight. A single unoptimized photograph can be 5-10MB straight from a camera, while the same image optimized for web can be 100-200KB with no perceptible quality difference.

The impact is direct:

  • Page load time — Every 100ms of delay reduces conversion rates by 7% (Google, 2023)
  • Core Web Vitals — Largest Contentful Paint (LCP) is heavily influenced by image load time
  • Bandwidth costs — At scale, unoptimized images cost real money in CDN bandwidth
  • User experience — Slow-loading images create visible layout shifts and frustration
  • Modern Image Formats

    AVIF (AV1 Image File Format)

    The most efficient format available today:

  • 50% smaller than JPEG at equivalent quality
  • 20% smaller than WebP
  • Supports transparency (alpha channel)
  • HDR and wide color gamut support
  • Browser support: Chrome 85+, Firefox 93+, Safari 16.4+
  • WebP

    Google's format that's now universally supported:

  • 25-35% smaller than JPEG
  • Supports transparency and animation
  • Excellent browser support (97%+ as of 2025)
  • Good fallback when AVIF isn't supported
  • Format Decision Tree

  • Can the browser handle AVIF? Use AVIF
  • Can it handle WebP? Use WebP
  • Fall back to JPEG (photos) or PNG (graphics with transparency)
  • Implementing Responsive Images

    The Picture Element

    The picture element lets you serve different formats and sizes based on the browser's capabilities:

    <picture>
    

    <source type="image/avif" srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w" sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 60vw" /> <source type="image/webp" srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w" sizes="(max-width: 640px) 100vw, (max-width: 1024px) 80vw, 60vw" /> <img src="hero-800.jpg" alt="Descriptive alt text" width="1200" height="675" loading="lazy" decoding="async" /> </picture>

    Key Attributes

  • srcset with width descriptors tells the browser which sizes are available
  • sizes tells the browser how large the image will be displayed at each viewport width
  • loading="lazy" defers loading for off-screen images
  • decoding="async" allows the browser to decode the image off the main thread
  • width and height attributes prevent layout shift (the browser reserves space before loading)
  • Automated Optimization Pipeline

    For a Vite-based project, you can automate image optimization during build:

    // vite.config.js
    

    import { defineConfig } from 'vite' import imagemin from 'vite-plugin-imagemin'

    export default defineConfig({ plugins: [ imagemin({ gifsicle: { optimizationLevel: 3 }, mozjpeg: { quality: 80 }, pngquant: { quality: [0.7, 0.85] }, webp: { quality: 80 }, avif: { quality: 50, speed: 4 } }) ] })

    Build Script for Batch Conversion

    For existing image assets, a Node.js script using sharp can process entire directories:

    import sharp from 'sharp'
    

    import { readdir } from 'fs/promises' import { join, parse } from 'path'

    const INPUT_DIR = './public/assets/images' const OUTPUT_DIR = './public/assets/optimized' const SIZES = [400, 800, 1200]

    async function processImages() { const files = await readdir(INPUT_DIR) const images = files.filter(f => /\.(jpg|jpeg|png)$/i.test(f) )

    for (const file of images) { const { name } = parse(file) const input = join(INPUT_DIR, file)

    for (const width of SIZES) { const base = sharp(input).resize(width)

    await base.clone() .avif({ quality: 50 }) .toFile(join(OUTPUT_DIR, ${name}-${width}.avif))

    await base.clone() .webp({ quality: 80 }) .toFile(join(OUTPUT_DIR, ${name}-${width}.webp))

    await base.clone() .jpeg({ quality: 82, mozjpeg: true }) .toFile(join(OUTPUT_DIR, ${name}-${width}.jpg)) } } }

    processImages()

    Lazy Loading Strategies

    Native Lazy Loading

    The simplest approach — just add loading="lazy" to any image below the fold:

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

    Intersection Observer (Custom Control)

    For more control over when images load and with loading animations:

    const observer = new IntersectionObserver((entries) => {
    

    entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target img.src = img.dataset.src img.classList.add('loaded') observer.unobserve(img) } }) }, { rootMargin: '200px' })

    document.querySelectorAll('img[data-src]') .forEach(img => observer.observe(img))

    The rootMargin: '200px' starts loading images 200px before they enter the viewport, ensuring a smooth experience.

    Measuring Results

    After optimization, measure the impact:

    Metric | Before | After | Improvement

    --- | --- | --- | ---

    Total image weight | 8.2 MB | 890 KB | 89% reduction

    LCP (mobile) | 4.2s | 1.8s | 57% faster

    Page load time | 6.1s | 2.3s | 62% faster

    Lighthouse Performance | 54 | 92 | +38 points

    Quick Wins Checklist

  • Use AVIF with WebP fallback for all photographs
  • Set explicit width and height on every image element
  • Add loading="lazy" to all below-the-fold images
  • Serve responsive sizes via srcset and sizes
  • Compress SVGs with SVGO (remove metadata, optimize paths)
  • Use CSS for decorative elements instead of images when possible
  • Enable CDN image optimization if available (Cloudflare, Vercel Image Optimization)
  • Conclusion

    Image optimization is one of the highest-impact performance improvements you can make. The modern web has excellent format support (AVIF, WebP), native lazy loading, and powerful build tools. Implementing even a few of these techniques can dramatically improve your site's speed, user experience, and search ranking.