Saltar al contenido

Imágenes responsivas: srcset, sizes y formatos modernos

Domina la entrega de imágenes responsivas con srcset, sizes, el elemento picture y formatos como AVIF y WebP: páginas más rápidas, igual calidad.

5 min de lectura
Comparación lado a lado que muestra la misma imagen principal servida en distintas resoluciones y formatos para dispositivos móviles, tablets y escritorio

Las imágenes representan la mayoría de los bytes transferidos en la mayoría de las páginas web. Servir una imagen principal de 2400px a una pantalla móvil de 375px desperdicia ancho de banda, hunde las puntuaciones de rendimiento y frustra a los usuarios con conexiones más lentas. Aun así, la mayoría de los desarrolladores sigue enviando un único tamaño de imagen y dando el trabajo por terminado.

Las imágenes responsivas resuelven esto al permitir que el navegador elija la imagen óptima según el ancho del viewport, la densidad de píxeles y el soporte de formatos. El HTML ya cuenta con las herramientas—srcset, sizes y el elemento <picture>. El reto está en entender cómo funcionan juntas.

El problema de las imágenes de una sola fuente

Una sola etiqueta <img> obliga a todos los dispositivos a descargar el mismo archivo, sin importar el tamaño de pantalla o las capacidades del dispositivo.

htmlhtml
<!-- ❌ One size for all devices -->
<img
  src="/images/hero-2400.jpg"
  alt="Mountain landscape at sunrise"
  width="2400"
  height="1600"
/>
<!--
  Mobile user on 3G: downloads 800KB for a 375px screen
  Desktop user on fiber: gets the right size by accident
  Neither gets modern formats like AVIF or WebP
-->
htmlhtml
<!-- ✅ Responsive with srcset and sizes -->
<img
  src="/images/hero-800.jpg"
  srcset="
    /images/hero-400.jpg   400w,
    /images/hero-800.jpg   800w,
    /images/hero-1200.jpg 1200w,
    /images/hero-1600.jpg 1600w,
    /images/hero-2400.jpg 2400w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 80vw,
    1200px
  "
  alt="Mountain landscape at sunrise"
  width="2400"
  height="1600"
  loading="lazy"
  decoding="async"
/>

El atributo srcset le indica al navegador qué anchos de imagen están disponibles. El atributo sizes le indica al navegador con qué ancho se renderizará la imagen en cada punto de ruptura. El navegador combina esta información con la densidad de píxeles del dispositivo para elegir el archivo óptimo.

Cómo funcionan srcset y sizes

El descriptor w en srcset comunica el ancho intrínseco de cada archivo de imagen. El navegador usa sizes para determinar el ancho renderizado y luego elige la mejor coincidencia.

htmlhtml
<!--
  How the browser decides:
 
  Device: iPhone 14 (390px viewport, 3x DPR)
  sizes says: at max-width 640px → 100vw → 390px
  Effective pixels needed: 390 × 3 = 1170px
  Browser picks: hero-1200.jpg (closest match ≥ 1170)
 
  Device: MacBook Pro (1440px viewport, 2x DPR)
  sizes says: at max-width 1024px? No.
  Falls through to default: 1200px
  Effective pixels needed: 1200 × 2 = 2400px
  Browser picks: hero-2400.jpg
-->
 
<img
  src="/images/hero-800.jpg"
  srcset="
    /images/hero-400.jpg   400w,
    /images/hero-800.jpg   800w,
    /images/hero-1200.jpg 1200w,
    /images/hero-1600.jpg 1600w,
    /images/hero-2400.jpg 2400w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 80vw,
    1200px
  "
  alt="Dashboard analytics view"
  width="2400"
  height="1600"
/>

Error común: fijar sizes en 100vw siempre. En escritorio, las imágenes dentro de un contenedor rara vez ocupan todo el ancho del viewport. Unos valores de sizes inexactos hacen que el navegador descargue imágenes demasiado grandes.

htmlhtml
<!-- ❌ sizes always 100vw wastes bandwidth on desktop -->
<img
  srcset="
    /images/card-400.jpg 400w,
    /images/card-800.jpg 800w
  "
  sizes="100vw"
  alt="Product card"
/>
 
<!-- ✅ Accurate sizes match actual layout -->
<img
  srcset="
    /images/card-400.jpg 400w,
    /images/card-800.jpg 800w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 50vw,
    33vw
  "
  alt="Product card"
/>

El elemento picture para la negociación de formatos

Mientras que srcset gestiona el cambio de resolución, <picture> gestiona la negociación de formatos y la dirección de arte. El navegador elige el primer <source> que sea compatible.

htmlhtml
<picture>
  <!-- AVIF: smallest file, newest format -->
  <source
    type="image/avif"
    srcset="
      /images/hero-400.avif   400w,
      /images/hero-800.avif   800w,
      /images/hero-1200.avif 1200w,
      /images/hero-1600.avif 1600w
    "
    sizes="(max-width: 640px) 100vw, 1200px"
  />
  <!-- WebP: good compression, wide support -->
  <source
    type="image/webp"
    srcset="
      /images/hero-400.webp   400w,
      /images/hero-800.webp   800w,
      /images/hero-1200.webp 1200w,
      /images/hero-1600.webp 1600w
    "
    sizes="(max-width: 640px) 100vw, 1200px"
  />
  <!-- JPEG fallback: universal support -->
  <img
    src="/images/hero-800.jpg"
    srcset="
      /images/hero-400.jpg   400w,
      /images/hero-800.jpg   800w,
      /images/hero-1200.jpg 1200w,
      /images/hero-1600.jpg 1600w
    "
    sizes="(max-width: 640px) 100vw, 1200px"
    alt="Mountain landscape at sunrise"
    width="1600"
    height="1067"
    loading="lazy"
    decoding="async"
  />
</picture>

AVIF normalmente logra archivos un 50% más pequeños que JPEG con una calidad visual equivalente. WebP se ubica entre ambos. Al listar AVIF primero, los navegadores compatibles obtienen los archivos más pequeños, mientras que los navegadores más antiguos recurren a WebP o JPEG.

Dirección de arte con media queries

A veces distintos viewports necesitan recortes de imagen fundamentalmente diferentes, no solo distintas resoluciones. Una imagen principal panorámica en escritorio podría necesitar un recorte cuadrado en móvil para mantener visible al sujeto.

htmlhtml
<picture>
  <!-- Mobile: square crop focused on subject -->
  <source
    media="(max-width: 640px)"
    srcset="
      /images/hero-mobile-400.avif  400w,
      /images/hero-mobile-800.avif  800w
    "
    sizes="100vw"
    type="image/avif"
  />
  <source
    media="(max-width: 640px)"
    srcset="
      /images/hero-mobile-400.webp  400w,
      /images/hero-mobile-800.webp  800w
    "
    sizes="100vw"
    type="image/webp"
  />
  <!-- Desktop: wide landscape -->
  <source
    srcset="
      /images/hero-desktop-1200.avif 1200w,
      /images/hero-desktop-1600.avif 1600w,
      /images/hero-desktop-2400.avif 2400w
    "
    sizes="100vw"
    type="image/avif"
  />
  <img
    src="/images/hero-desktop-1200.jpg"
    srcset="
      /images/hero-desktop-1200.jpg 1200w,
      /images/hero-desktop-1600.jpg 1600w,
      /images/hero-desktop-2400.jpg 2400w
    "
    sizes="100vw"
    alt="Team collaborating in an open office"
    width="2400"
    height="800"
  />
</picture>

Automatización de la generación de imágenes con Sharp

Crear manualmente múltiples tamaños y formatos no es sostenible. Automatiza el proceso con Sharp en un script de compilación.

tstypescript
import sharp from "sharp";
import { readdir, mkdir } from "node:fs/promises";
import { join, parse } from "node:path";
 
const INPUT_DIR = "images/originals";
const OUTPUT_DIR = "public/images";
const WIDTHS = [400, 800, 1200, 1600, 2400];
const FORMATS = ["avif", "webp", "jpg"] as const;
 
type ImageFormat = (typeof FORMATS)[number];
 
const FORMAT_OPTIONS: Record<ImageFormat, object> = {
  avif: { quality: 60, effort: 6 },
  webp: { quality: 75, effort: 5 },
  jpg: { quality: 80, mozjpeg: true },
};
 
async function processImage(
  inputPath: string
): Promise<void> {
  const { name } = parse(inputPath);
  const image = sharp(inputPath);
  const metadata = await image.metadata();
  const originalWidth = metadata.width ?? 2400;
 
  for (const format of FORMATS) {
    for (const width of WIDTHS) {
      if (width > originalWidth) continue;
 
      const outputPath = join(
        OUTPUT_DIR,
        `${name}-${width}.${format}`
      );
 
      await sharp(inputPath)
        .resize(width, null, {
          withoutEnlargement: true,
          fit: "inside",
        })
        .toFormat(format, FORMAT_OPTIONS[format])
        .toFile(outputPath);
 
      console.log(`  ✅ ${name}-${width}.${format}`);
    }
  }
}
 
async function main() {
  await mkdir(OUTPUT_DIR, { recursive: true });
 
  const files = await readdir(INPUT_DIR);
  const images = files.filter((f) =>
    /\.(jpg|jpeg|png|tiff)$/i.test(f)
  );
 
  console.log(
    `Processing ${images.length} images...\n`
  );
 
  for (const file of images) {
    console.log(`📸 ${file}`);
    await processImage(join(INPUT_DIR, file));
  }
 
  console.log("\n✅ All images processed");
}
 
main();

Implementación en React/Next.js

Los frameworks modernos suelen abstraer las imágenes responsivas, pero entender el HTML subyacente ayuda cuando esa abstracción se queda corta.

tsxtsx
// ResponsiveImage.tsx
interface ImageProps {
  src: string; // base name without extension
  alt: string;
  width: number;
  height: number;
  sizes: string;
  priority?: boolean;
  className?: string;
}
 
const WIDTHS = [400, 800, 1200, 1600, 2400];
 
function buildSrcSet(
  base: string,
  format: string
): string {
  return WIDTHS.map(
    (w) => `/images/${base}-${w}.${format} ${w}w`
  ).join(", ");
}
 
export function ResponsiveImage({
  src,
  alt,
  width,
  height,
  sizes,
  priority = false,
  className,
}: ImageProps) {
  return (
    <picture>
      <source
        type="image/avif"
        srcSet={buildSrcSet(src, "avif")}
        sizes={sizes}
      />
      <source
        type="image/webp"
        srcSet={buildSrcSet(src, "webp")}
        sizes={sizes}
      />
      <img
        src={`/images/${src}-800.jpg`}
        srcSet={buildSrcSet(src, "jpg")}
        sizes={sizes}
        alt={alt}
        width={width}
        height={height}
        loading={priority ? "eager" : "lazy"}
        decoding={priority ? "sync" : "async"}
        fetchPriority={priority ? "high" : "auto"}
        className={className}
      />
    </picture>
  );
}
 
// Usage
<ResponsiveImage
  src="hero-landscape"
  alt="Mountain landscape at sunrise"
  width={2400}
  height={1600}
  sizes="(max-width: 640px) 100vw, 1200px"
  priority
/>

Puntos clave

Usa srcset con descriptores de ancho (w) para ofrecer múltiples resoluciones de imagen y sizes para indicarle al navegador el ancho renderizado en cada punto de ruptura—unos valores de sizes precisos son fundamentales porque el navegador los usa para calcular qué imagen descargar antes de que ocurra el layout. El elemento <picture> gestiona la negociación de formatos listando las fuentes en orden de preferencia (AVIF primero, luego WebP, y JPEG como respaldo), permitiendo que cada navegador descargue el formato más eficiente que soporte. La dirección de arte requiere elementos <source> separados con atributos media cuando distintos viewports necesitan recortes diferentes—el cambio de resolución con srcset solo escala la misma imagen, no cambia la composición. Automatiza la generación de imágenes con herramientas como Sharp para crear todas las variantes de tamaño y formato a partir de los archivos fuente durante la compilación, eliminando el procesamiento manual de imágenes y garantizando un resultado consistente en cada imagen del proyecto.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX