Crear una pipeline de optimización de imágenes con Claude Code
Automatiza WebP/AVIF, imágenes responsivas y presupuestos de CI con Claude Code.
Optimizar imágenes no significa comprimir una carpeta justo antes de publicar. En cuanto un sitio tiene hero images, capturas de artículos, miniaturas de productos, diagramas y tarjetas sociales, el proceso manual deja huecos. Una sola imagen PNG sin tratar puede convertirse en el elemento de Largest Contentful Paint y hacer que toda la página parezca lenta.
En esta guía usaremos Claude Code como compañero de implementación para crear una pipeline repetible: sharp genera variantes AVIF, WebP y JPEG; un componente responsivo las entrega con picture; y una verificación de CI bloquea imágenes que superan el presupuesto. El objetivo no es conseguir el archivo más pequeño a cualquier precio, sino mantener legibilidad, compatibilidad, nombres predecibles y una revisión técnica clara.
La primera prueba de Masa en un blog técnico fue demasiado simple: “genera AVIF y listo”. Bajó el peso, pero algunos crawlers seguían necesitando JPEG, las capturas de código se veían borrosas con calidad demasiado baja y la imagen principal quedó con lazy loading por error. La solución estable apareció cuando separó conversión, renderizado y verificación.
Si todavía estás aprendiendo el flujo base, empieza por la guía inicial de Claude Code. Para revisar rendimiento más allá de las imágenes, combínala con optimización de rendimiento con Claude Code.
Vista general
No le pidas a Claude Code solo “haz las imágenes más rápidas”. Dale una estructura que pueda implementar y que tú puedas revisar por partes.
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"]
El script de conversión crea variantes deterministas. El componente informa al navegador qué candidatos existen. La comprobación de presupuesto evita que un archivo pesado llegue a producción. Esta separación también reduce el riesgo al usar Claude Code, porque cada cambio queda dentro de un diff manejable.
Reglas de calidad antes de escribir código
No empieces con un único número de calidad. Una foto, una captura de interfaz, un diagrama y una imagen OGP fallan de formas distintas. Antes de generar código, conviene pasar a Claude Code una tabla como esta.
| Uso | Objetivo | Revisión |
|---|---|---|
| Hero image | 1280px o más, AVIF/WebP primero, JPEG de respaldo | Puede ser el LCP, debe cargarse con prioridad |
| Captura en artículo | Variantes 640px/960px | El texto pequeño debe seguir legible |
| Galería o listado | Variantes 320px/640px | Lazy loading fuera del primer viewport |
| Imagen social | Mantener JPEG o PNG | Algunos crawlers no usan formatos modernos |
En esta actualización de junio de 2026, la referencia principal para formatos sigue siendo la documentación oficial de sharp. Para HTML, sigue la guía de imágenes responsivas de MDN: srcset funciona bien solo cuando sizes describe el ancho real de renderizado.
Implementación 1: generar variantes con sharp
Este script lee jpg, jpeg y png desde public/images/original, escribe los resultados en public/images/optimized y crea un manifest.json para las verificaciones posteriores.
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);
});
El detalle importante es no ampliar imágenes pequeñas. Si una captura de 900px se guarda con un nombre 1280w, la depuración futura será confusa. El manifest deja visible el ancho real y el tamaño en bytes de cada salida.
Implementación 2: componente responsivo
Después de generar archivos, necesitamos entregar candidatos al navegador. Este componente sirve para React; en Astro se puede usar la misma estructura de picture, source e img.
// 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>
);
}
Usa priority solo para la imagen principal visible al cargar. Si todas las imágenes se cargan con eager, compiten con CSS, JavaScript, fuentes y el verdadero elemento LCP. La guía de LCP en web.dev ayuda a decidir qué imagen merece prioridad.
Implementación 3: presupuesto de imágenes en CI
La pipeline debe poder fallar sola. Los revisores ven el diseño, pero rara vez revisan el peso de cada archivo generado. Este script lee el manifest y detiene el build si una variante grande supera el límite.
// 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"
}
}
Puedes empezar con un límite simple, por ejemplo 240KB. Más adelante conviene separar presupuesto para hero images, capturas y miniaturas, porque no todas tienen la misma función.
Tres casos de uso reales
El primer caso es un blog técnico. Las capturas suelen partir de PNG de alta resolución. Si le dices a Claude Code que el ancho de lectura ronda 960px y que el texto pequeño debe conservarse, propondrá mejor sizes y valores de calidad.
El segundo caso es una landing de SaaS. La captura del producto suele ser el LCP, por lo que necesita width, height, prioridad solo para esa imagen y lazy loading para las demás.
El tercer caso es una galería de ecommerce o portfolio. La misma foto aparece en tarjeta, detalle, carrusel y OGP. El manifest permite saber qué variantes existen y facilita pruebas automáticas.
Errores frecuentes
No reduzcas demasiado la calidad de AVIF. Una foto puede verse aceptable, pero una captura con texto pequeño puede quedar ilegible.
No omitas sizes. Sin ese atributo, el navegador puede asumir que la imagen ocupa todo el viewport y descargar una variante más grande de lo necesario.
No pongas lazy loading en la imagen principal del primer viewport. Es útil para contenido inferior, pero puede empeorar LCP si se aplica al hero.
No pidas a Claude Code CDN, panel de administración, migración de framework y conversión de imágenes en el mismo prompt. Primero el script, luego el componente, después la verificación.
Prompt práctico 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.
El prompt define ubicación, formatos, reglas, metadatos y alcance. Esa claridad hace que el resultado de Claude Code sea más fácil de revisar.
Resultado verificado
En la prueba de Masa, reemplazar capturas PNG de 1920px por esta pipeline redujo a menos de la mitad la transferencia de imágenes en artículos. La prueba fallida fue usar AVIF con calidad 45 en capturas de código: ahorró bytes, pero el texto quedó blando. La configuración estable fue AVIF alrededor de 50 para fotos, revisión visual de WebP/JPEG en capturas de UI y prioridad solo para el hero.
El siguiente paso es ejecutar npm run images:build y npm run images:check sobre una sola categoría de imágenes. Cuando el flujo sea estable, conéctalo con automatización de flujos en Claude Code para detectar regresiones en Pull Requests.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Permission receipt para Claude Code: alcance, prueba y rollback
Patrón de permission receipt para Claude Code: acciones permitidas, aprobación, pruebas, rollback y CTA de ingresos.
Agent Harness seguro para Claude Code y Codex: permisos, verificacion y rollback
Diseña un Agent Harness seguro para Claude Code y Codex con permisos, plan, verificaciones y rollback.
Subagentes de Claude Code: guía práctica para delegar trabajo de forma segura
Guía práctica de subagentes en Claude Code para dividir artículos y código: reglas, prompts, riesgos y checklist.