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
<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
// 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
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
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
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
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
// 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
// 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
// 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.

