Saltar al contenido

Optimización de imágenes para el rendimiento web

Guía práctica de optimización de imágenes para la web: formatos modernos, imágenes responsivas, carga diferida, CDN y su impacto en Core Web Vitals.

5 min de lectura
Comparación de antes y después que muestra la reducción del tamaño del archivo de imagen de 2 MB a 40 KB conservando la calidad visual

Las imágenes representan la mayor parte de los bytes transferidos en la mayoría de las páginas web. Una imagen principal sin optimizar puede arruinar por sí sola tu puntuación de Largest Contentful Paint. La buena noticia es que la optimización de imágenes es una de las mejoras de rendimiento con mayor impacto y menor esfuerzo que puedes aplicar. Los formatos modernos, el dimensionamiento responsivo y la carga diferida pueden reducir el peso de las imágenes entre un 70 % y un 90 % sin pérdida visible de calidad.

El objetivo no es hacer que las imágenes sean lo más pequeñas posible, sino entregar la imagen correcta, en el tamaño correcto, en el formato correcto y en el momento correcto.

Formatos modernos de imagen

WebP y AVIF ofrecen una compresión notablemente mejor que JPEG y PNG, manteniendo la calidad visual. La compatibilidad con navegadores ya es lo bastante amplia como para usarlos como formato principal.

htmlhtml
<!-- ❌ Serving a single JPEG format to all browsers -->
<img src="/photos/hero.jpg" alt="Product showcase" />
<!-- 2.1 MB — unoptimized, one size for all screens -->
 
<!-- ✅ Serving modern formats with fallback -->
<picture>
  <!-- AVIF: smallest file size, 94% browser support (2024) -->
  <source srcset="/photos/hero.avif" type="image/avif" />
  <!-- WebP: good compression, 97% browser support -->
  <source srcset="/photos/hero.webp" type="image/webp" />
  <!-- JPEG: universal fallback -->
  <img src="/photos/hero.jpg" alt="Product showcase"
       width="1200" height="600"
       loading="eager"
       fetchpriority="high" />
</picture>
<!-- AVIF: ~180 KB, WebP: ~280 KB, JPEG: ~2.1 MB -->
<!-- Same visual quality, 87% smaller with AVIF -->
shbash
# Convert images to modern formats using sharp-cli
# Install: npm install -g sharp-cli
 
# JPEG → WebP (quality 80 is visually lossless for photos)
sharp -i hero.jpg -o hero.webp --format webp --quality 80
 
# JPEG → AVIF (quality 60 is comparable to WebP quality 80)
sharp -i hero.jpg -o hero.avif --format avif --quality 60
 
# PNG → WebP (for images with transparency)
sharp -i logo.png -o logo.webp --format webp --quality 90
 
# Batch convert all JPEGs in a directory
for f in *.jpg; do
  sharp -i "$f" -o "${f%.jpg}.webp" --format webp --quality 80
  sharp -i "$f" -o "${f%.jpg}.avif" --format avif --quality 60
done

Imágenes responsivas con srcset

Servir una imagen de 2000 px de ancho a una pantalla móvil de 400 px desperdicia ancho de banda. Las imágenes responsivas permiten que el navegador elija el tamaño adecuado según el ancho de pantalla y la densidad de píxeles.

htmlhtml
<!-- ❌ One image size for all devices -->
<img src="/photos/hero-2000w.jpg" alt="Product showcase" />
<!-- Mobile downloads 2000px image, displays at 400px — wasteful -->
 
<!-- ✅ Multiple sizes — browser picks the best one -->
<img
  srcset="
    /photos/hero-400w.webp   400w,
    /photos/hero-800w.webp   800w,
    /photos/hero-1200w.webp 1200w,
    /photos/hero-2000w.webp 2000w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 80vw,
    60vw
  "
  src="/photos/hero-1200w.webp"
  alt="Product showcase"
  width="1200"
  height="600"
/>
<!-- Mobile downloads 400w (50KB), desktop downloads 2000w (280KB) -->
tstypescript
// Build script to generate responsive image variants
import sharp from 'sharp';
import { readdir } from 'fs/promises';
import { join, parse } from 'path';
 
const WIDTHS = [400, 800, 1200, 2000];
const FORMATS: Array<{ ext: string; options: object }> = [
  { ext: 'avif', options: { quality: 60 } },
  { ext: 'webp', options: { quality: 80 } },
];
 
async function generateResponsiveImages(inputDir: string, outputDir: string) {
  const files = await readdir(inputDir);
 
  for (const file of files) {
    const { name } = parse(file);
    const inputPath = join(inputDir, file);
 
    for (const width of WIDTHS) {
      for (const format of FORMATS) {
        const outputPath = join(outputDir, `${name}-${width}w.${format.ext}`);
 
        await sharp(inputPath)
          .resize(width, null, { withoutEnlargement: true })
          .toFormat(format.ext as keyof sharp.FormatEnum, format.options)
          .toFile(outputPath);
 
        console.log(`Generated: ${outputPath}`);
      }
    }
  }
}
 
generateResponsiveImages('./originals', './public/images');

Carga diferida y sugerencias de prioridad

No todas las imágenes deben cargarse de inmediato. Las imágenes que aparecen en la parte visible inicial de la pantalla necesitan una carga inmediata con alta prioridad. Las que quedan más abajo deben cargarse de forma diferida: el navegador las solicita solo cuando se acercan al viewport.

htmlhtml
<!-- Above the fold: load immediately with high priority -->
<img
  src="/photos/hero.webp"
  alt="Hero banner"
  width="1200" height="600"
  loading="eager"
  fetchpriority="high"
  decoding="async"
/>
 
<!-- Below the fold: lazy load when approaching viewport -->
<img
  src="/photos/feature-1.webp"
  alt="Feature showcase"
  width="600" height="400"
  loading="lazy"
  decoding="async"
/>
tstypescript
// ❌ Lazy loading ALL images — including the hero
// The hero image is the Largest Contentful Paint element
// Lazy loading it DELAYS LCP, making performance worse
<img src="/hero.webp" loading="lazy" /> // Don't do this
 
// ✅ Eager load above-the-fold, lazy load everything else
// Rule of thumb: the first 1-2 images should be eager
// Everything below the initial viewport should be lazy
htmlhtml
<!-- Preload critical hero image for fastest LCP -->
<head>
  <link
    rel="preload"
    as="image"
    href="/photos/hero.avif"
    type="image/avif"
    fetchpriority="high"
  />
</head>
<!-- This tells the browser to fetch the hero image immediately,
     before it even parses the <img> tag in the body -->

Cómo evitar el desplazamiento de diseño

Las imágenes sin dimensiones explícitas provocan desplazamientos de diseño: el contenido salta a medida que las imágenes se cargan. Esto arruina las puntuaciones de CLS (Cumulative Layout Shift).

htmlhtml
<!-- ❌ No dimensions — causes layout shift as image loads -->
<img src="/photos/product.webp" alt="Product" />
<!-- Browser doesn't know the size until image loads → content jumps -->
 
<!-- ✅ Explicit width and height — browser reserves space -->
<img
  src="/photos/product.webp"
  alt="Product"
  width="600"
  height="400"
  style="max-width: 100%; height: auto;"
/>
<!-- Browser calculates aspect ratio from width/height -->
<!-- Reserves exact space before image loads → zero layout shift -->
csscss
/* Modern approach: aspect-ratio property */
.product-image {
  width: 100%;
  aspect-ratio: 3 / 2;  /* Maintains ratio without height attribute */
  object-fit: cover;     /* Fills container without distortion */
}
 
/* Placeholder blur while loading */
.image-container {
  position: relative;
  background-color: #e2e8f0;
  background-image: url('data:image/svg+xml,...'); /* Tiny blur placeholder */
  background-size: cover;
}
 
.image-container img {
  width: 100%;
  height: auto;
  transition: opacity 0.3s;
}
 
.image-container img[loading] {
  opacity: 0;
}

Cómo medir el impacto con los Core Web Vitals

La optimización de imágenes afecta directamente a dos Core Web Vitals: LCP (Largest Contentful Paint) y CLS (Cumulative Layout Shift). Mide antes y después para cuantificar la mejora.

tstypescript
// Measure LCP in the browser
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP:', entry.startTime, 'ms');
    console.log('Element:', (entry as any).element?.tagName);
    // If this is an <img>, your image optimization directly affects this number
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });
 
// Measure CLS contributions
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!(entry as any).hadRecentInput) {
      console.log('Layout shift:', (entry as any).value);
      for (const source of (entry as any).sources ?? []) {
        console.log('Shifted element:', source.node?.tagName);
        // Images without width/height appear here
      }
    }
  }
}).observe({ type: 'layout-shift', buffered: true });
ymlyaml
# Before and after optimization — real numbers
before_optimization:
  hero_image_size: "2.1 MB (JPEG, 3000x1500)"
  total_image_payload: "8.4 MB"
  lcp: "4.2 seconds"
  cls: "0.28"
 
after_optimization:
  hero_image_size: "180 KB (AVIF, responsive srcset)"
  total_image_payload: "1.2 MB"
  lcp: "1.8 seconds"      # 57% improvement
  cls: "0.02"              # 93% improvement
 
changes_made:
  - "Converted to AVIF/WebP with picture element"
  - "Generated 4 responsive sizes per image"
  - "Added width/height to all img tags"
  - "Lazy loaded below-fold images"
  - "Preloaded hero image in document head"

Puntos clave

  1. Usa AVIF como formato principal y WebP como alternativa — AVIF genera archivos hasta un 50 % más pequeños que JPEG con una calidad similar, con un 94 % de compatibilidad con navegadores
  2. Genera variantes de imagen responsivas en 400w, 800w, 1200w y 2000w — deja que el navegador elija según el tamaño de pantalla
  3. Carga de inmediato las imágenes de la parte visible inicial y difiere el resto — y precarga la imagen de LCP en el head del documento
  4. Define siempre los atributos width y height en las imágenes para evitar desplazamientos de diseño y mantener un CLS de cero
  5. Mide LCP y CLS antes y después — la optimización de imágenes es una de las mejoras con mayor retorno que puedes aplicar
  6. Automatiza con un pipeline de compilación — sharp u otras herramientas similares generan todos los formatos y tamaños a partir de las imágenes originales
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX