Améliorer le Largest Contentful Paint (LCP) passe souvent par une optimisation drastique des images. Dans beaucoup de projets, j'ai constaté que la génération d'images seulement au build time n'est pas toujours suffisante : on veut des images adaptées à la taille d'écran, au format supporté par le navigateur, avec une livraison rapide via CDN. Dans cet article je décris comment j'automatise la génération d'images adaptatives en production avec sharp et Vercel, tout en garantissant un meilleur LCP.

Pourquoi générer les images à la volée plutôt qu'à la main ?

Générer manuellement toutes les variantes d'une image (différentes largeurs et formats) devient vite ingérable, surtout pour des sites avec du contenu dynamique ou des médias fréquents. L'approche "on-demand" permet :

  • de produire uniquement les tailles réellement demandées par des visiteurs,
  • d'adapter le format selon l'Accept-header du navigateur (AVIF/WebP/JPEG),
  • de réduire l'espace de stockage et le coût lié aux générations inutiles,
  • d'assurer des images optimisées pour le LCP sans surcharger le build.
  • Architecture que j'utilise

    Voici l'architecture qui fonctionne bien pour moi :

  • Sources d'images stockées dans un bucket (S3, DigitalOcean Spaces) ou directement dans le repo pour les assets statiques.
  • Un endpoint serverless sur Vercel (API Route ou Edge Function) qui :
  • - reçoit l'URL de l'image et des paramètres (width, quality, format),
  • - récupère l'image source (cache upstream),
  • - transforme avec sharp,
  • - renvoie l'image transformée avec des en-têtes cache/CDN adéquats.
  • Le CDN de Vercel met ensuite en cache la réponse pour les requêtes suivantes.

    Paramètres recommandés

    Pour obtenir un bon compromis qualité/poids, j'utilise typiquement :

  • breakpoints : 320, 480, 768, 1024, 1366, 1920
  • formats : prioriser AVIF puis WebP puis JPEG/PNG
  • quality : entre 60 et 80 selon le type d'image
  • Voici un tableau récapitulatif des breakpoints que j'applique souvent :

    Breakpoint (px) Usage
    320 mobiles petits
    480 mobiles classiques
    768 tablettes portrait
    1024 tablettes paysage / petits laptops
    1366 laptops
    1920 écrans larges / hero

    Comment je construis l'API sur Vercel

    Concrètement, l'API reçoit des paramètres via query string comme ?url=...&w=1024&q=75. L'algorithme principal :

  • valider et assainir l'URL source (ne pas transformer n'importe quel hôte pour éviter un proxy ouvert),
  • récupérer l'image source (fetch depuis le bucket),
  • passer le flux à sharp :
  • sharp permet :

  • resize (width),
  • conversion de format (toFormat('avif' | 'webp' | 'jpeg') ),
  • définir quality, chroma subsampling, strip metadata pour réduire la taille.
  • Enfin je renvoie l'image avec des headers :

  • Content-Type adéquat (image/avif ou image/webp),
  • Cache-Control: public, max-age=31536000, s-maxage=31536000, immutable pour que Vercel CDN et les navigateurs stockent efficacement les images,
  • une surcouche si besoin pour l'invalidation (clé de versioning).
  • Optimiser le LCP : techniques pratiques

    Pour améliorer le LCP, il ne suffit pas d'avoir une image légère : il faut la rendre prioritaire et rapide à obtenir.

  • Preload de l'image hero : j'ajoute un <link rel="preload" as="image" href="..." imagesrcset="..." imagesizes="..."> pour que le navigateur commence la requête le plus tôt possible.
  • Utiliser l'attribut priority (dans Next.js) ou charger l'URL du hero directement dans le HTML pour éviter un fetch JS tardif.
  • Générer l'URL de l'image adaptative côté serveur (SSR) de sorte que le HTML initial contienne déjà la balise img avec le bon srcset.
  • S'il s'agit d'une page statique, pré-générer les srcset pour le hero en build-time et fallback vers l'API on-demand pour les autres images.
  • Détecter le format supporté (Accept header)

    Je lis l'en-tête Accept côté serveur. Exemple de logique :

  • si accept contient "image/avif" → produire AVIF,
  • sinon si contient "image/webp" → produire WebP,
  • sinon produire JPEG/PNG.
  • Cela diminue encore la taille et donc le temps de téléchargement pour la majorité des navigateurs modernes.

    Cache, invalidation et coûts

    Quelques recommandations issues de mon expérience :

  • activer le cache CDN (s-maxage) pour éviter de recomposer l'image à chaque requête,
  • mettre une clé versionnée dans l'URL (ex: ?v=2026-10-01) pour forcer une invalidation globale si nécessaire,
  • éviter les transformations trop coûteuses côté runtime (par ex. upscaling massif), et limiter la mémoire/timeouts des fonctions serverless,
  • pour les pics de trafic, être conscient des coûts liés au CPU (sharp est CPU-bound). Si nécessaire, pré-générer les images les plus demandées.
  • Mesurer l'impact

    Je teste systématiquement avec :

  • Lighthouse (Mobile, 3G/Slow CPU) pour voir l'impact sur LCP,
  • WebPageTest pour visualiser le filmstrip et vérifier le moment où le hero est rendu,
  • mes propres métriques RUM (Core Web Vitals) pour vérifier l'effet en production.
  • En pratique, migrer vers une génération adaptative et des formats modernes (AVIF/WebP) réduit souvent le poids des images critiques de 30-70% et fait gagner plusieurs centaines de millisecondes sur le LCP.

    Pièges à éviter

  • ne pas sécuriser l'endpoint : vérifiez l'origine des images pour éviter un proxy ouvert utilisé pour le scraping,
  • ne pas oublier les headers Cache-Control : sans eux, vous perdrez l'avantage du CDN,
  • éviter de faire du streaming inutilement si votre fonction serverless ne gère pas bien les gros flux,
  • surveiller la latence entre votre bucket et la fonction Vercel : rapprochez les entrepôts d'images du datacenter Vercel si possible.
  • Exemple de workflow rapide

    Voici le flux que j'applique :

  • le HTML SSR contient une balise img avec srcset pointant vers mon endpoint Vercel et des widths pré-calculés,
  • le navigateur demande la ressource (préload si hero),
  • Vercel récupère l'original depuis le storage, transforme avec sharp selon l'Accept, met les headers cache et renvoie,
  • le CDN met en cache la réponse et la diffuse très rapidement aux visiteurs suivants.
  • Si vous voulez, je peux vous fournir un exemple de code de l'API Vercel (sans oublier les points de sécurité) ou une checklist pour intégrer ce système à Next.js. Dites-moi votre stack (Next.js, Nuxt, SvelteKit, site statique...) et j'adapte l'exemple.