Advanced (Atualizado: 02/06/2026)

Criar um pipeline de otimização de imagens com Claude Code

Automatize WebP/AVIF, imagens responsivas e orçamento de CI com Claude Code.

Criar um pipeline de otimização de imagens com Claude Code

Otimização de imagens não é apenas comprimir uma pasta antes de publicar. Quando um site passa a ter imagem hero, capturas em artigos, miniaturas de produtos, diagramas e imagens para redes sociais, o processo manual fica inconsistente. Uma única PNG esquecida pode virar o elemento de Largest Contentful Paint e piorar a percepção de velocidade da página inteira.

Neste guia, usamos Claude Code como parceiro de implementação para criar um pipeline repetível. O sharp gera variantes AVIF, WebP e JPEG, um componente responsivo com picture entrega as opções ao navegador, e uma verificação de CI bloqueia arquivos acima do orçamento. A meta não é perseguir o menor arquivo possível, e sim equilibrar legibilidade, compatibilidade, nomes previsíveis e revisão técnica simples.

A primeira tentativa de Masa em um pequeno blog técnico foi direta demais: “gerar AVIF e publicar”. O peso caiu, mas alguns crawlers ainda precisavam de JPEG, capturas de código ficaram borradas com qualidade muito baixa, e a imagem hero acabou com lazy loading por engano. O fluxo só ficou sustentável quando conversão, renderização e verificação foram separados.

Se você ainda está começando, leia primeiro o guia inicial de Claude Code. Para revisar desempenho além de imagens, combine com otimização de performance com Claude Code.

Visão geral

Não peça apenas “deixe as imagens mais rápidas”. Dê ao Claude Code um fluxo claro, com uma responsabilidade por etapa.

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"]

O script de conversão cria variantes determinísticas. O componente informa os candidatos ao navegador. A verificação de orçamento impede que imagens pesadas cheguem à produção. Essa divisão também torna o trabalho do Claude Code mais auditável, porque cada solicitação gera um diff menor.

Regras de qualidade antes do código

Um erro comum é aplicar o mesmo valor de qualidade para todos os tipos de imagem. Fotos, screenshots de interface, diagramas e imagens OGP degradam de maneiras diferentes. Antes de pedir código, costumo passar a Claude Code uma tabela como esta.

UsoObjetivoPonto de revisão
Imagem hero1280px ou mais, AVIF/WebP primeiro, JPEG de fallbackCandidata a LCP, carregar com prioridade
Screenshot de artigoVariantes 640px/960pxTexto pequeno precisa continuar legível
Galeria ou listaVariantes 320px/640pxLazy loading fora do primeiro viewport
Imagem socialManter JPEG ou PNGCompatibilidade com crawlers

Nesta atualização de junho de 2026, a principal referência para formatos é a documentação oficial do sharp. No HTML, siga o guia de imagens responsivas da MDN: srcset só funciona bem quando sizes descreve a largura real de exibição.

Implementação 1: gerar variantes com sharp

O script abaixo lê jpg, jpeg e png de public/images/original, grava resultados em public/images/optimized e cria um manifest.json para a verificação de 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);
});

O ponto essencial é não ampliar imagens pequenas. Se um screenshot de 900px for salvo como 1280w, o nome do arquivo passa uma informação falsa e dificulta a depuração. O manifest guarda largura real e bytes de cada saída.

Implementação 2: componente responsivo

Depois da geração, precisamos entregar as opções corretas ao navegador. Este exemplo é para React; em Astro, a mesma estrutura com picture, source e img pode ser usada em um componente .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>
  );
}

Use priority apenas para a imagem principal visível ao carregar a página. Se todas as imagens forem eager, elas competem com CSS, JavaScript, fontes e o elemento real de LCP. O guia de LCP da web.dev ajuda a decidir qual imagem merece prioridade.

Implementação 3: orçamento de imagens no CI

Um pipeline confiável precisa falhar automaticamente. Revisores olham o layout, mas raramente conferem o tamanho de cada arquivo gerado. O script abaixo lê o manifest e bloqueia variantes grandes acima do limite.

// 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"
  }
}

Comece com uma regra simples, como 240KB. Depois, divida o orçamento por tipo: hero, screenshot e thumbnail não precisam ter o mesmo limite.

Três casos de uso práticos

O primeiro caso é um blog técnico. Screenshots costumam sair como PNGs grandes de telas de alta resolução. Ao dizer a Claude Code qual é a largura do conteúdo, a largura mobile e a necessidade de preservar texto pequeno, o resultado de sizes e qualidade melhora.

O segundo caso é uma landing page SaaS. A imagem do produto ou hero costuma ser o LCP. Ela precisa de width, height, carregamento prioritário e fallback; as demais imagens continuam com lazy loading.

O terceiro caso é uma galeria de ecommerce ou portfólio. A mesma imagem aparece em card, página de detalhe, carrossel e OGP. O manifest mostra quais variantes existem e facilita testes.

Armadilhas comuns

Não reduza demais a qualidade AVIF. Fotos podem parecer aceitáveis, enquanto screenshots de interface ficam difíceis de ler.

Não omita sizes. Sem ele, o navegador pode assumir que a imagem ocupa toda a largura do viewport e baixar uma variante grande demais.

Não coloque lazy loading na imagem hero. Lazy loading é útil abaixo da primeira dobra, mas atrasa o elemento visível que pode definir o LCP.

Não peça a Claude Code CDN, painel administrativo, migração de framework e conversão de imagens no mesmo prompt. Faça script, componente e CI em etapas separadas.

Prompt para 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.

Esse prompt define caminhos, formatos, regras, metadados e escopo. Quanto mais claro o limite, mais fácil revisar o resultado do Claude Code.

Resultado verificado

No teste de Masa, trocar screenshots PNG brutos de 1920px por este pipeline reduziu a transferência de imagens dos artigos para menos da metade. A tentativa ruim foi usar AVIF qualidade 45 em capturas de código: economizou bytes, mas deixou o texto suave demais. A configuração estável foi AVIF na faixa baixa dos 50 para fotos, revisão visual de WebP/JPEG para UI e prioridade apenas na imagem hero.

O próximo passo é executar npm run images:build e npm run images:check em uma única categoria de imagens. Quando o fluxo estiver estável, conecte-o com automação de workflow no Claude Code para detectar regressões em Pull Requests.

#Claude Code #otimização de imagens #WebP #AVIF #performance
Grátis

PDF grátis: cheatsheet do Claude Code

Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.

Cuidamos dos seus dados e não enviamos spam.

Masa

Sobre o autor

Masa

Engenheiro focado em workflows práticos com Claude Code.