Aller au contenu principal
    IT INNOVE - Logo de l'agence de développement web et mobile
    Retour au blog
    Performance

    Optimisation d'images web: formats et compression

    Équipe IT INNOVE
    7 min de lecture

    WebP, AVIF et techniques de compression pour des temps de chargement optimaux.

    Optimisation d'images web: formats et compression - Performance | IT INNOVE
    Optimisation d'images web: formats et compression - Performance

    Optimisation d'images web: formats et compression

    Les images représentent 60% du poids des pages web. Une optimisation méthodique peut réduire les temps de chargement de 40-70% sans perte visuelle notable.

    Impact business des images optimisées

    **Cas client e-commerce (500+ produits)
    **
    • Poids moyen par page: 3.2MB → 0.9MB (-72%)
    • LCP: 4.1s → 1.8s (-56%)
    • Taux de conversion mobile: +34%
    • Bounce rate: -28%

    Formats modernes et support navigateur

    Ordre de priorité 2024

    html
    <picture>
      <!-- AVIF: meilleure compression (Chrome 85+, Firefox 93+) -->
      <source srcset="/hero.avif" type="image/avif">
      
      <!-- WebP: bon compromis (96% support navigateur) -->
      <source srcset="/hero.webp" type="image/webp">
      
      <!-- JPEG fallback universel -->
      <img src="/hero.jpg" alt="Description" loading="lazy">
    </picture>

    Comparaison formats par use case

    | Format | Compression | Transparence | Animation | Use case optimal |
    |--------|-------------|--------------|-----------|------------------|
    | AVIF | Excellent (-50% vs JPEG) | ✅ | ✅ | Photos haute qualité |
    | WebP | Très bon (-25% vs JPEG) | ✅ | ✅ | Usage général |
    | JPEG | Correct | ❌ | ❌ | Fallback photo |
    | PNG | Faible | ✅ | ❌ | Logos, icônes |
    | SVG | Excellent | ✅ | ✅ | Illustrations vectorielles |

    Pipeline d'optimisation automatisé

    Script Sharp.js pour la génération

    js
    // scripts/optimize-images.js
    const sharp = require('sharp');
    const fs = require('fs').promises;
    const path = require('path');
    
    async function generateResponsiveImages(inputPath, outputDir) {
      const sizes = [400, 800, 1200, 1600, 2400];
      const formats = ['avif', 'webp', 'jpeg'];
      
      const basename = path.parse(inputPath).name;
      
      for (const size of sizes) {
        for (const format of formats) {
          const outputPath = path.join(outputDir, `${basename}-${size}w.${format}`);
          
          await sharp(inputPath)
            .resize(size, null, {
              withoutEnlargement: true,
              fit: 'inside'
            })
            .toFormat(format, {
              quality: format === 'jpeg' ? 85 : 80,
              effort: format === 'avif' ? 6 : 4, // Max compression
              progressive: format === 'jpeg'
            })
            .toFile(outputPath);
            
          console.log(`Generated: ${outputPath}`);
        }
      }
    }
    
    // Usage
    generateResponsiveImages('./src/hero.jpg', './public/images/optimized/');

    Composant React responsive

    tsx
    interface ResponsiveImageProps {
      src: string; // Base filename without extension
      alt: string;
      sizes?: string;
      priority?: boolean;
      className?: string;
    }
    
    export function ResponsiveImage({ 
      src, 
      alt, 
      sizes = "100vw", 
      priority = false,
      className 
    }: ResponsiveImageProps) {
      const basePath = `/images/optimized/${src}`;
      
      // Générer les srcsets pour chaque format
      const avifSrcSet = [400, 800, 1200, 1600, 2400]
        .map(size => `${basePath}-${size}w.avif ${size}w`)
        .join(', ');
        
      const webpSrcSet = [400, 800, 1200, 1600, 2400]
        .map(size => `${basePath}-${size}w.webp ${size}w`)
        .join(', ');
        
      const jpegSrcSet = [400, 800, 1200, 1600, 2400]
        .map(size => `${basePath}-${size}w.jpeg ${size}w`)
        .join(', ');
    
      return (
        <picture>
          <source
            srcSet={avifSrcSet}
            sizes={sizes}
            type="image/avif"
          />
          <source
            srcSet={webpSrcSet}
            sizes={sizes}
            type="image/webp"
          />
          <img
            srcSet={jpegSrcSet}
            sizes={sizes}
            src={`${basePath}-800w.jpeg`}
            alt={alt}
            loading={priority ? "eager" : "lazy"}
            decoding="async"
            className={className}
          />
        </picture>
      );
    }

    Optimisation par contexte

    Images produits e-commerce

    tsx
    function ProductImage({ product }: { product: Product }) {
      return (
        <ResponsiveImage
          src={`products/${product.slug}`}
          alt={`Photo du produit ${product.name}`}
          sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
          priority={false}
          className="aspect-square object-cover rounded-lg"
        />
      );
    }

    Images hero avec Critical Path

    tsx
    function HeroSection() {
      return (
        <section className="relative h-screen">
          <ResponsiveImage
            src="hero-homepage"
            alt="Développement d'applications sur mesure"
            sizes="100vw"
            priority={true} // Preload critique
            className="absolute inset-0 w-full h-full object-cover"
          />
          
          {/* Overlay content */}
          <div className="relative z-10 flex items-center justify-center h-full">
            <h1>Votre vision, notre expertise</h1>
          </div>
        </section>
      );
    }

    Lazy loading intelligent

    Intersection Observer avec préchargement

    tsx
    import { useEffect, useRef, useState } from 'react';
    
    function useLazyImage(src: string, threshold = 0.1) {
      const [isLoaded, setIsLoaded] = useState(false);
      const [isInView, setIsInView] = useState(false);
      const imgRef = useRef<HTMLImageElement>(null);
    
      useEffect(() => {
        const observer = new IntersectionObserver(
          ([entry]) => {
            if (entry.isIntersecting) {
              setIsInView(true);
              observer.disconnect();
            }
          },
          { 
            threshold,
            rootMargin: '50px' // Précharger 50px avant d'être visible
          }
        );
    
        if (imgRef.current) {
          observer.observe(imgRef.current);
        }
    
        return () => observer.disconnect();
      }, [threshold]);
    
      return { isInView, isLoaded, setIsLoaded, imgRef };
    }
    
    function LazyImage({ src, alt, className }: ImageProps) {
      const { isInView, isLoaded, setIsLoaded, imgRef } = useLazyImage(src);
    
      return (
        <div className="relative overflow-hidden">
          {/* Placeholder avec blur */}
          <div 
            className={`absolute inset-0 bg-gray-200 transition-opacity duration-300 ${
              isLoaded ? 'opacity-0' : 'opacity-100'
            }`}
            style={{
              backgroundImage: `url(data:image/svg+xml;base64,${btoa(`
                <svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
                  <rect width="100%" height="100%" fill="#f3f4f6"/>
                  <text x="50%" y="50%" text-anchor="middle" fill="#9ca3af">Loading...</text>
                </svg>
              `)})`,
              backgroundSize: 'cover'
            }}
          />
          
          {/* Image réelle */}
          {isInView && (
            <ResponsiveImage
              ref={imgRef}
              src={src}
              alt={alt}
              className={className}
              onLoad={() => setIsLoaded(true)}
            />
          )}
        </div>
      );
    }

    CDN et optimisations réseau

    Configuration Cloudflare Images

    js
    // next.config.js
    module.exports = {
      images: {
        loader: 'custom',
        loaderFile: './src/utils/cloudflareLoader.js'
      }
    };
    
    // src/utils/cloudflareLoader.js
    export default function cloudflareLoader({ src, width, quality = 75 }) {
      const params = new URLSearchParams({
        format: 'auto', // AVIF si supporté, sinon WebP
        width: width.toString(),
        quality: quality.toString(),
        fit: 'scale-down'
      });
      
      return `https://imagedelivery.net/YOUR-ACCOUNT-ID/${src}/${params}`;
    }

    Headers HTTP optimaux

    js
    // netlify.toml
    [[headers]]
      for = "/images/*"
      [headers.values]
        Cache-Control = "public, max-age=31536000, immutable"
        Vary = "Accept"
    
    [[redirects]]
      from = "/images/*.jpg"
      to = "/images/:splat.webp"
      status = 302
      conditions = {Accept = "image/webp"}

    Monitoring et métriques

    Script de mesure des performances images

    js
    // scripts/measure-image-performance.js
    const puppeteer = require('puppeteer');
    
    async function analyzeImagePerformance(url) {
      const browser = await puppeteer.launch();
      const page = await browser.newPage();
      
      // Capturer les métriques réseau
      const images = [];
      page.on('response', response => {
        if (response.request().resourceType() === 'image') {
          images.push({
            url: response.url(),
            size: response.headers()['content-length'],
            format: response.headers()['content-type'],
            status: response.status()
          });
        }
      });
      
      await page.goto(url, { waitUntil: 'networkidle2' });
      
      // Mesurer LCP
      const lcp = await page.evaluate(() => {
        return new Promise((resolve) => {
          new PerformanceObserver((list) => {
            const entries = list.getEntries();
            const lastEntry = entries[entries.length - 1];
            resolve(lastEntry.startTime);
          }).observe({ entryTypes: ['largest-contentful-paint'] });
        });
      });
      
      await browser.close();
      
      const totalImageSize = images.reduce((sum, img) => sum + (parseInt(img.size) || 0), 0);
      
      return {
        lcp,
        totalImages: images.length,
        totalSize: totalImageSize,
        averageSize: totalImageSize / images.length,
        modernFormats: images.filter(img => 
          img.format?.includes('webp') || img.format?.includes('avif')
        ).length
      };
    }
    
    // Rapport d'optimisation
    analyzeImagePerformance('https://votre-site.com').then(metrics => {
      console.log('📊 Analyse des images:');
      console.log(`LCP: ${metrics.lcp}ms`);
      console.log(`Images totales: ${metrics.totalImages}`);
      console.log(`Poids total: ${(metrics.totalSize / 1024).toFixed(2)} KB`);
      console.log(`Formats modernes: ${metrics.modernFormats}/${metrics.totalImages}`);
      
      if (metrics.lcp > 2500) {
        console.warn('⚠️  LCP trop élevé - optimiser les images hero');
      }
      
      if (metrics.modernFormats / metrics.totalImages < 0.8) {
        console.warn('⚠️  Moins de 80% de formats modernes');
      }
    });

    Checklist optimisation images

    Pre-production

    • [ ] Pipeline Sharp.js configuré
    • [ ] Formats AVIF/WebP générés
    • [ ] Srcsets responsifs implémentés
    • [ ] Lazy loading avec préchargement
    • [ ] Placeholders blur/skeleton

    Production

    • [ ] CDN configuré avec compression
    • [ ] Cache headers optimaux (1 an)
    • [ ] Monitoring Core Web Vitals
    • [ ] A/B test qualité vs taille
    • [ ] Audit régulier (Lighthouse)

    L'optimisation d'images est un investissement technique qui paie immédiatement en UX et conversion. Une approche systématique évite les optimisations ponctuelles et garantit des gains durables.

    Questions fréquentes