Mejora el rendimiento de tu web: Optimización de imágenes

Piedra y pluma representando la optimización de imágenes

Si tus imágenes pesan como un elefante mojado, tu negocio sangra por mil cortes invisibles. Esto no va de plugins mágicos ni de “sube el JPG y ya”. Va de estrategia, cirugía y propósito. Bienvenido al frente.

Acto I — La mentira de la medianía

Dogma reproducido

  • “Sube las imágenes en JPG a 80% y activa lazy-loading: suficiente.”
  • “El CDN ya optimiza solo. No perdamos tiempo.”
  • “El diseño manda, luego ya comprimimos si hace falta.”

Autopsia

  • Síntoma visible: homepage con héroe a pantalla completa, carrusel de logos y tres tarjetas de producto. Carga “percibida” lenta en móvil; el scroll tartamudea; CLS salta cuando aparece el héroe.
  • Causa raíz: imágenes exportadas desde Figma a 2x sin recorte de área útil; PNG donde bastaba JPG; sin width/height; sin srcset/sizes; logos en PNG con fondo blanco; héroe sin prioridad ni preload; uso de background-image para contenido esencial.
  • Cadena de decisiones: “tenemos prisa” → “ya lo arreglaremos” → “pon lazy-loading a todo” → “el CDN hará magia” → deuda creciendo y nadie midiendo.

Evidencia mínima

  • Peso total imágenes (home, móvil): 4,6 MB → coste de datos medio en 3G ≈ 0,03 € por visita en mercados caros.
  • LCP p75 (móvil): 4,1 s. Objetivo ≤ 2,5 s. Penalización SEO y conversión.
  • CLS: 0,19 por no fijar dimensiones. Umbral recomendado ≤ 0,1.

Smells

  • Todos los img sin width/height. CLS casi garantizado.
  • PNG para fotografías, y logos rasterizados a 2000 px “por si acaso”.
  • Un único src para todos los anchos; nada de srcset/sizes; “lazy” hasta para el héroe.

Contraejemplo rápido

  • Antes (5 líneas)
HTML

<img src="/img/hero.jpg" alt="Nuevo modelo">
<img src="/img/logo1.png">
<img src="/img/card.jpg">
<img src="/img/card2.jpg">
<img src="/img/card3.jpg">
  • Después (5 líneas)
HTML

<link rel="preload" as="image" 
imagesrcset="/img/hero-1200.avif 1200w, /img/hero-800.avif 800w" imagesizes="(max-width:768px) 100vw, 50vw">
<img src="/img/hero-1200.avif" srcset="/img/hero-800.avif 800w, /img/hero-1200.avif 1200w" sizes="100vw" width="1200" height="800" fetchpriority="high" alt="Nuevo modelo">
<img src="/img/logo1.svg" width="120" height="40" loading="lazy">
<img src="/img/card-600.webp" srcset="/img/card-400.webp 400w, /img/card-600.webp 600w" sizes="(max-width:600px) 100vw, 600px" loading="lazy" decoding="async">
<!-- LCP baja ~35–55% en móvil, CLS≈0 -->
  • Regla de salida: si te has reconocido aquí, pon precio al daño: cada 0,5 s de LCP perdido y cada MB extra es fricción que convierte menos y se posiciona peor. Ya no es estética; son euros.

Acto II — La verdad

Visión

  • Una imagen en la web no es un archivo: es una decisión de entrega. Optimizar no es “comprimir”, es hacer llegar la intención visual adecuada con el menor coste de tiempo, datos y CPU. Diseño decide el porqué; la ingeniería orquesta el cómo. La estrategia manda: formato, tamaño, prioridad, momento y canal.

Modelo operativo (cadena de mando)

  • Contenido → ¿vector, foto, decorativo, crítico para la tarea?
  • Formato → SVG/AVIF/WebP/JPEG/PNG según contenido y compatibilidad.
  • Tamañocrop semántico, múltiples anchos, densidades, srcset/sizes.
  • Entregapreload/fetchpriority, decoding, loading, caché/CDN.
  • Presentaciónwidth/height, aspect-ratio, object-fit, image-set en CSS.
  • Control → métricas: LCP/CLS/transfer size/hits de caché. Automatiza o muere.

Tácticas de campo

  • 1) Elegir formato por intención
    • Haz: SVG para logos/íconos; AVIF para fotos/hero; WebP como reserva; PNG solo si necesitas transparencia sin banding o píxeles nítidos.
    • No uses: PNG para fotografía; JPG para logos; AVIF con grano extremo en UGC altamente comprimido (artefactos feos).
  • 2) Responsive de verdad con srcset/sizes
    • Haz: usa descriptores w y un sizes honesto al layout. Declara width/height para CLS≈0.
    • No uses: un único 2x gigante “por si acaso”; sizes genérico que siempre devuelve el mayor recurso.
  • 3) Prioriza como si fuera una evacuación
    • Haz: preload del LCP con imagesrcset/imagesizes; fetchpriority=»high» en el LCP; decoding=»async» en lo demás.
    • No uses: loading=»lazy» en el LCP; preloads redundantes sin srcset (derroche).
  • 4) Fondo ≠ contenido
    • Haz: si es decorativo, background-image con image-set() y media queries.
    • No uses: fondos para contenido con significado; pierdes alt, semántica y SEO.
  • 5) Automatiza el pipeline
    • Haz: genera variantes (ancho/calidad/formato) con sharp, ImageMagick o el CDN; purga EXIF, convierte a sRGB, limita calidad por métrica (SSIM/DSSIM).
    • No uses: exportar a mano “a ojo” en cada sprint.

Demostración mínima (10 minutos, reproducible)

  • Objetivo: convertir una foto héroe 2400×1600 PNG (1,8 MB) a AVIF/WebP en 3 anchos (800/1200/1600), integrarla con preload, medir mejora.
  • Entrada: hero.png.
  • Pasos:
    • Instala sharp y genera variantes.
JS

import sharp from 'sharp';
const sizes = [800, 1200, 1600];
for (const w of sizes) {
  await sharp('hero.png').resize({ width: w }).toFormat('avif', { quality: 45 }).toFile(`hero-${w}.avif`);
  await sharp('hero.png').resize({ width: w }).toFormat('webp', { quality: 70 }).toFile(`hero-${w}.webp`);
}
  • Integra en HTML (LCP con prioridad, fallback a WebP).
HTML

<link rel="preload" as="image" imagesrcset="/img/hero-800.avif 800w, /img/hero-1200.avif 1200w, /img/hero-1600.avif 1600w" imagesizes="100vw">
<picture>
  <source type="image/avif" srcset="/img/hero-800.avif 800w, /img/hero-1200.avif 1200w, /img/hero-1600.avif 1600w" sizes="100vw">
  <source type="image/webp" srcset="/img/hero-800.webp 800w, /img/hero-1200.webp 1200w, /img/hero-1600.webp 1600w" sizes="100vw">
  <img src="/img/hero-1200.webp" width="1200" height="800" alt="Colección Otoño" fetchpriority="high">
</picture>
  • Fija el espacio y usa image-set solo en decorativos.
CSS

/* CLS a cero con dimensiones y relación */
.hero { aspect-ratio: 3 / 2; }
.banner--decorativo {
 background-image: 
image-set(url('/img/bg-1x.avif') 1x, 
url('/img/bg-2x.avif') 2x); 
background-size: 
cover; }
  • Salida esperada: peso del héroe ≈ 140–260 KB (según ancho), LCP p75 baja ~30–50% en móvil, CLS ≈ 0 por width/height y aspect-ratio.

Trade-offs

  • Más ficheros y rutas (varias variantes) → compensa con automatización y CDN.
  • Compatibilidad AVIF en navegadores antiguos → usa picture con fallback WebP/JPEG.
  • Preload mal usado puede derrochar ancho de banda → restringe al LCP y declara imagesizes.

Impacto en negocio

  • Time-to-change: cambiar una imagen global tarda minutos, no horas, al estar automatizado.
  • Coherencia de marca: nitidez/recorte consistentes; nada de logos borrosos ni fondos lavados.
  • Rendimiento/retención: menos rebote en móvil, mejor Core Web Vitals, mejor SEO y conversión.

Acto III — El manifiesto

Principios no negociables

  • Decide el propósito antes del formato.
  • SVG para identidad, AVIF/WebP para foto, PNG solo si está justificado.
  • Todo img lleva width y height o aspect-ratio.
  • El LCP tiene preload y fetchpriority=»high»; nada de lazy.
  • Siempre srcset + sizes con descriptores w, nunca un 2x único.
  • Recorta al motivo: no entregues píxeles invisibles.
  • Purgar EXIF, normalizar a sRGB y limitar calidad por métrica objetiva.
  • Automatiza la generación de variantes en CI/CD.
  • Decorativo en CSS, semántico en HTML con alt significativo.
  • Mide cada cambio: bytes, LCP, CLS, hit-ratio de caché.

Definición de Hecho (DoD)

  • Imágenes nuevas con srcset/sizes y width/height declarados.
  • Formato acorde a contenido (SVG/AVIF/WebP/JPEG/PNG) con fallback si procede.
  • Héroe y above the fold con preload + fetchpriority.
  • Decorativos sin texto embebido; alt vacíos si no aportan significado; alt descriptivo si lo tienen.
  • Pipeline genera ≥3 anchos y elimina metadatos; commit incluye script/config.
  • Se adjuntan métricas: tamaño transferido y efecto en LCP/CLS.

Métricas de guardarraíl

  • Peso total imágenes/home (móvil) ≤ 900 KB p75.
  • LCP p75 (móvil) ≤ 2,5 s.
  • CLS ≤ 0,1.
  • Hit-ratio CDN ≥ 85% para imágenes.
  • Variantes por imagen: al menos 3 anchos reales servidos.

Coto de caza (anti-patrones prohibidos)

  • PNG para fotografías “porque se ve mejor”. No entra.
  • Imágenes sin width/height. CLS garantizado: rechazado.
  • loading=»lazy» en el LCP. No.
  • Un único src gigante para todos los anchos. No.
  • Texto dentro de imágenes para titulares o CTA. Accesibilidad y SEO a la basura.

Plan de acción de 24 h

  • 1 Audita la home: lista de imágenes, uso (crítico/decorativo), formato, tamaño. Métrica: peso total actual.
  • 2 Monta script de variantes (sharp/ImageMagick) y genera 800/1200/1600 en AVIF+WebP. Métrica: bytes ahorrados.
  • 3 Implementa srcset/sizes y declara width/height en todos los img. Métrica: CLS tras cambio.
  • 4 Añade preload + fetchpriority al LCP. Métrica: LCP p75 en laboratorio (Lighthouse) y real si tienes RUM.
  • 5 Pasa logos a SVG o WebP nítido; mueve decorativos a CSS con image-set(). Métrica: número de recursos y caché CDN.

Cierre

  • Esta no es una guía de filtros y sliders. Es un sistema de decisiones con dientes. Apaga el piloto automático, mide, automatiza y entrega intención, no megabytes. El resto es ruido.