Advanced (Mis à jour: 02/06/2026)

Créer un pipeline d'optimisation d'images avec Claude Code

Automatisez WebP/AVIF, images responsives et contrôle de budget CI avec Claude Code.

Créer un pipeline d'optimisation d'images avec Claude Code

L’optimisation d’images ne se résume pas à compresser un dossier juste avant la mise en production. Dès qu’un site contient une image hero, des captures dans les articles, des miniatures de produits, des schémas et des visuels sociaux, le travail manuel devient fragile. Une seule capture PNG oubliée peut devenir l’élément Largest Contentful Paint et ralentir toute la perception de la page.

Dans ce guide, Claude Code sert de partenaire d’implémentation pour construire un pipeline répétable. Nous générons des variantes AVIF, WebP et JPEG avec sharp, nous les servons via un composant picture responsive, puis nous ajoutons une vérification CI qui bloque les fichiers trop lourds. Le but n’est pas d’obtenir le fichier le plus petit à tout prix, mais de garder des images lisibles, compatibles, nommées de manière prévisible et faciles à auditer.

La première tentative de Masa sur un petit blog technique était trop directe : “produire de l’AVIF et publier”. Le poids a baissé, mais certains crawlers avaient encore besoin de JPEG, les captures de code devenaient floues avec une qualité trop agressive, et l’image hero avait été chargée en lazy loading par erreur. Le pipeline est devenu fiable seulement après avoir séparé conversion, rendu et vérification.

Si vous débutez avec l’outil, commencez par le guide de démarrage Claude Code. Pour un audit de performance plus large, complétez avec l’optimisation de performance avec Claude Code.

Vue d’ensemble

Ne demandez pas simplement à Claude Code de “rendre les images plus rapides”. Donnez-lui un flux précis, avec une responsabilité claire pour chaque fichier.

flowchart LR
  A["original images"] --> B["sharp conversion"]
  B --> C["AVIF / WebP / JPEG variants"]
  C --> D["OptimizedImage component"]
  D --> E["browser chooses best source"]
  C --> F["manifest.json"]
  F --> G["CI size budget check"]

Le script de conversion crée des variantes déterministes. Le composant donne les bons candidats au navigateur. Le contrôle de budget empêche une image trop lourde d’arriver en production. Cette séparation rend aussi le travail de Claude Code plus sûr, car chaque demande produit une différence limitée et vérifiable.

Définir les règles de qualité

Le piège classique consiste à appliquer une seule valeur de qualité à toutes les images. Une photo, une capture d’interface, un diagramme et une image OGP ne se dégradent pas de la même façon. Avant de générer du code, je donne généralement ce cadre à Claude Code.

UsageObjectifPoint de revue
Image hero1280px ou plus, AVIF/WebP d’abord, JPEG en secoursCandidat LCP, chargement prioritaire
Capture d’articleVariantes 640px/960pxLe petit texte doit rester lisible
Galerie ou listeVariantes 320px/640pxLazy loading hors premier écran
Visuel socialJPEG ou PNG conservéCompatibilité avec certains crawlers

Pour cette mise à jour de juin 2026, la référence principale pour les formats reste la documentation officielle de sharp. Côté HTML, suivez le guide MDN sur les images responsives : srcset doit être accompagné d’un sizes réaliste, sinon le navigateur peut choisir un fichier trop grand.

Implémentation 1 : générer les variantes

Le script suivant lit les fichiers jpg, jpeg et png dans public/images/original, écrit les résultats dans public/images/optimized, puis produit un manifest.json utilisé par la vérification CI.

npm install -D sharp glob tsx
// scripts/optimize-images.ts
import path from "node:path";
import { mkdir, writeFile } from "node:fs/promises";
import { glob } from "glob";
import sharp from "sharp";

const inputDir = process.argv[2] ?? "public/images/original";
const outputDir = process.argv[3] ?? "public/images/optimized";
const widths = [320, 640, 960, 1280, 1920] as const;
const formats = ["avif", "webp", "jpeg"] as const;
const quality = { avif: 52, webp: 76, jpeg: 82 } as const;

type ImageFormat = (typeof formats)[number];
type ManifestEntry = {
  src: string;
  width: number;
  format: string;
  bytes: number;
};

const manifest: Record<string, ManifestEntry[]> = {};

function slugFromPath(filePath: string) {
  const relative = path.relative(inputDir, filePath);
  return relative
    .replace(path.extname(relative), "")
    .split(path.sep)
    .join("-")
    .replace(/[^a-zA-Z0-9_-]/g, "-")
    .toLowerCase();
}

function extension(format: ImageFormat) {
  return format === "jpeg" ? "jpg" : format;
}

async function buildVariant(filePath: string, slug: string, width: number, format: ImageFormat) {
  let image = sharp(filePath).rotate().resize({ width, withoutEnlargement: true });

  if (format === "avif") image = image.avif({ quality: quality.avif, effort: 4 });
  if (format === "webp") image = image.webp({ quality: quality.webp, effort: 4 });
  if (format === "jpeg") image = image.jpeg({ quality: quality.jpeg, mozjpeg: true });

  const fileName = `${slug}-${width}w.${extension(format)}`;
  const target = path.join(outputDir, fileName);
  const info = await image.toFile(target);

  return {
    src: `/images/optimized/${fileName}`,
    width: info.width,
    format: extension(format),
    bytes: info.size,
  };
}

async function optimizeOne(filePath: string) {
  const metadata = await sharp(filePath).metadata();
  const sourceWidth = metadata.width ?? widths[widths.length - 1];
  const targetWidths: number[] = widths.filter((width) => width <= sourceWidth);

  if (!targetWidths.includes(sourceWidth)) targetWidths.push(sourceWidth);
  targetWidths.sort((a, b) => a - b);

  const slug = slugFromPath(filePath);
  manifest[slug] = [];

  for (const width of targetWidths) {
    for (const format of formats) {
      manifest[slug].push(await buildVariant(filePath, slug, width, format));
    }
  }

  console.log(`optimized ${slug}: ${manifest[slug].length} files`);
}

async function main() {
  await mkdir(outputDir, { recursive: true });

  const pattern = `${inputDir.replace(/\\/g, "/")}/**/*.{jpg,jpeg,png}`;
  const files = await glob(pattern, { nodir: true });

  for (const filePath of files) {
    await optimizeOne(filePath);
  }

  await writeFile(
    path.join(outputDir, "manifest.json"),
    JSON.stringify(manifest, null, 2),
  );

  console.log(`done: ${files.length} source images`);
}

void main().catch((error) => {
  console.error(error);
  process.exit(1);
});

Le détail important est de ne pas agrandir les sources. Si une capture de 900px est enregistrée sous un nom 1280w, le nom du fichier devient trompeur. Le manifest garde la largeur réelle et le poids en octets, ce qui rend la revue beaucoup plus concrète.

Implémentation 2 : composant responsive

Après la génération, il faut exposer les bons candidats au navigateur. Cet exemple est écrit pour React ; dans Astro, la même structure picture, source et img peut être placée dans un composant Astro.

// src/components/OptimizedImage.tsx
import type { ImgHTMLAttributes } from "react";

type OptimizedImageProps = Omit<
  ImgHTMLAttributes<HTMLImageElement>,
  "src" | "srcSet" | "sizes" | "width" | "height" | "loading"
> & {
  slug: string;
  alt: string;
  width: number;
  height: number;
  widths?: number[];
  sizes?: string;
  priority?: boolean;
};

function srcSet(slug: string, widths: number[], extension: "avif" | "webp" | "jpg") {
  return widths
    .map((width) => `/images/optimized/${slug}-${width}w.${extension} ${width}w`)
    .join(", ");
}

export function OptimizedImage({
  slug,
  alt,
  width,
  height,
  widths = [320, 640, 960, 1280],
  sizes = "(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 960px",
  priority = false,
  className,
  ...imgProps
}: OptimizedImageProps) {
  const fallbackWidth = widths.includes(960) ? 960 : widths[Math.floor(widths.length / 2)];
  const priorityProps = priority
    ? ({ fetchPriority: "high" } as ImgHTMLAttributes<HTMLImageElement>)
    : {};

  return (
    <picture className={className}>
      <source type="image/avif" srcSet={srcSet(slug, widths, "avif")} sizes={sizes} />
      <source type="image/webp" srcSet={srcSet(slug, widths, "webp")} sizes={sizes} />
      <img
        src={`/images/optimized/${slug}-${fallbackWidth}w.jpg`}
        srcSet={srcSet(slug, widths, "jpg")}
        sizes={sizes}
        width={width}
        height={height}
        alt={alt}
        loading={priority ? "eager" : "lazy"}
        decoding={priority ? "sync" : "async"}
        {...priorityProps}
        {...imgProps}
      />
    </picture>
  );
}

Réservez priority à l’image principale visible au chargement. Si toutes les images deviennent eager, elles se disputent le réseau avec le CSS, JavaScript, les polices et le vrai élément LCP. Le guide LCP de web.dev donne un bon cadre de décision.

Implémentation 3 : budget CI

Un pipeline fiable doit pouvoir échouer automatiquement. Les reviewers voient les problèmes visuels, mais ils ne vérifient pas toujours le poids de chaque variante. Ce script lit le manifest et bloque les fichiers trop lourds.

// scripts/check-image-budget.mjs
import { readFile } from "node:fs/promises";

const manifestUrl = new URL("../public/images/optimized/manifest.json", import.meta.url);
const manifest = JSON.parse(await readFile(manifestUrl, "utf8"));
const maxBytes = Number(process.env.IMAGE_BUDGET_BYTES ?? 240_000);
const failures = [];

for (const [slug, entries] of Object.entries(manifest)) {
  for (const entry of entries) {
    const isLargeCandidate = entry.width >= 1280 && ["avif", "webp", "jpg"].includes(entry.format);
    if (isLargeCandidate && entry.bytes > maxBytes) {
      failures.push(`${slug} ${entry.width}w.${entry.format}: ${entry.bytes} bytes`);
    }
  }
}

if (failures.length > 0) {
  console.error(`Image budget exceeded. Limit: ${maxBytes} bytes`);
  for (const failure of failures) console.error(`- ${failure}`);
  process.exit(1);
}

console.log("Image budget check passed.");
{
  "scripts": {
    "images:build": "tsx scripts/optimize-images.ts",
    "images:check": "node scripts/check-image-budget.mjs"
  }
}

Commencez avec une limite simple, par exemple 240KB. Ensuite, séparez les budgets : une image hero peut avoir une tolérance différente d’une miniature ou d’une capture de documentation.

Trois cas d’usage

Premier cas : le blog technique. Les captures sont souvent de gros PNG issus d’un écran haute résolution. En indiquant à Claude Code la largeur du contenu, la largeur mobile et l’exigence de lisibilité, vous obtenez de meilleurs sizes et une qualité plus adaptée.

Deuxième cas : la landing page SaaS. La capture produit ou l’image hero est souvent le LCP. Elle doit donc avoir width, height, un chargement prioritaire, et les autres images doivent rester en lazy loading.

Troisième cas : la galerie e-commerce ou portfolio. La même source peut apparaître dans une carte, une page détail, un carrousel et une image OGP. Le manifest devient utile pour savoir quelles variantes existent.

Pièges fréquents

Ne baissez pas trop la qualité AVIF. Une photo peut sembler correcte alors qu’une capture d’interface devient illisible.

N’oubliez pas sizes. Sans lui, le navigateur peut supposer que l’image occupe toute la largeur du viewport et choisir une variante trop grande.

Ne mettez pas l’image hero en lazy loading. Le lazy loading est utile sous la ligne de flottaison, pas pour l’élément visible qui définit le LCP.

Ne demandez pas à Claude Code de gérer CDN, administration, migration de framework et conversion d’images dans un seul prompt. Script, composant, puis CI : ce séquencement donne de meilleures revues.

Prompt Claude Code

Create an image optimization script for jpg/png files in public/images/original.
Output files to public/images/optimized.
Generate 320, 640, 960, 1280, and 1920px widths in avif, webp, and jpg.
Do not generate a width larger than the original image.
Write manifest.json with src, width, format, and bytes.
Add package scripts named images:build and images:check.
Keep the diff minimal and do not touch unrelated files.

Ce prompt fixe les chemins, les formats, les règles et la méthode de vérification. Plus le cadre est explicite, plus Claude Code produit un résultat facile à relire.

Résultat vérifié

Dans le test de Masa, remplacer des captures PNG brutes de 1920px par ce pipeline a réduit de plus de moitié le transfert d’images dans les articles. L’essai raté était une qualité AVIF à 45 sur des captures de code : le poids baissait, mais le texte devenait mou. Le réglage stable a été AVIF autour de 50 pour les photos, vérification visuelle WebP/JPEG pour les captures d’UI, et priorité uniquement pour l’image hero.

La prochaine étape consiste à lancer npm run images:build et npm run images:check sur une seule catégorie d’images. Une fois le flux stable, reliez-le à l’automatisation de workflow avec Claude Code pour détecter les régressions dans les Pull Requests.

#Claude Code #optimisation d'images #WebP #AVIF #performance
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.