Este artículo se centra en buenas prácticas de Web Performance. Optimizar el rendimiento de las imágenes no exime de cuidar la accesibilidad: atributos como
altson imprescindibles y quedan fuera del scope de esta guía.
El elemento <img> parece simple, pero hay una diferencia enorme entre usarlo a medias y usarlo bien. En mis auditorías de rendimiento es uno de los elementos donde más oportunidades de mejora encuentro: imágenes sin lazy loading, sin width y height declarados, con prioridad incorrecta para el LCP… Errores que cuestan puntos en Core Web Vitals y, sobre todo, peor experiencia para quien navega.
En este artículo recorro los atributos y técnicas más importantes, desde imágenes responsivas hasta la optimización del Largest Contentful Paint.
Table of Contents
Open Table of Contents
Imágenes responsivas con srcset y sizes
El atributo srcset permite declarar varias versiones de una misma imagen para que el navegador elija la más adecuada según el contexto (densidad de pantalla, tamaño del viewport).
<!-- ❌ Un solo tamaño para todos -->
<img src="hero.jpg" alt="Hero image" />
<!-- ✅ Versiones para distintas densidades y tamaños -->
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
width="800"
height="600"
alt="Hero image"
/>
El atributo sizes le indica al navegador cuánto espacio ocupará la imagen en el layout para cada breakpoint. Sin sizes, el navegador asume 100vw y puede descargar una imagen más grande de la necesaria.
Formatos modernos con <picture>
Los formatos modernos como AVIF o WebP ofrecen una compresión significativamente mejor que JPEG o PNG. El elemento <picture> permite declarar múltiples fuentes: el navegador carga el primero que soporta.
JPEG XL también promete mejoras notables de compresión, pero su soporte es muy limitado: Chrome lo eliminó en 2022 y solo está disponible de forma experimental en Firefox y Safari. Por ahora, AVIF + WebP es la combinación más segura.
<!-- ❌ Solo JPEG, sin alternativas -->
<img src="photo.jpg" alt="Foto" />
<!-- ✅ AVIF primero, WebP como fallback, JPEG como base -->
<picture>
<source type="image/avif" srcset="photo.avif" />
<source type="image/webp" srcset="photo.webp" />
<img src="photo.jpg" width="800" height="600" alt="Foto" />
</picture>
El orden importa: el navegador prueba de arriba a abajo y usa el primero que puede renderizar. AVIF ofrece la mejor compresión, pero con menor soporte que WebP. JPEG queda como fallback universal.
Combinando responsividad y formatos modernos
Podemos combinar ambas técnicas para entregar la imagen correcta en formato y tamaño:
<picture>
<source
type="image/avif"
srcset="photo-400.avif 400w, photo-800.avif 800w, photo-1600.avif 1600w"
sizes="(max-width: 600px) 100vw, 50vw"
/>
<source
type="image/webp"
srcset="photo-400.webp 400w, photo-800.webp 800w, photo-1600.webp 1600w"
sizes="(max-width: 600px) 100vw, 50vw"
/>
<img
src="photo-800.jpg"
srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 50vw"
width="800"
height="600"
alt="Foto"
/>
</picture>
Sí, es más código. Pero los CDN de imágenes como Cloudinary resuelven esto automáticamente detectando el formato óptimo mediante el header Accept de la petición, sin que tengamos que escribir ninguna de estas variantes a mano.
Atributos de rendimiento: loading, decoding y fetchpriority
Estos tres atributos son los que más impacto tienen en las métricas de rendimiento y los que más frecuentemente veo mal configurados.
loading
Controla cuándo el navegador descarga la imagen.
<!-- ❌ Carga todas las imágenes al inicio, aunque estén fuera del viewport -->
<img src="photo.jpg" alt="Foto" />
<!-- ✅ Difiere la carga de imágenes fuera del viewport -->
<img src="photo.jpg" alt="Foto" loading="lazy" />
loading="lazy" es seguro para cualquier imagen que no sea visible en el primer viewport. Para las que sí lo son —especialmente la imagen LCP— hay que usar loading="eager" (o simplemente no declarar el atributo, que es el valor por defecto).
width y height
Declarar las dimensiones en el HTML permite al navegador reservar el espacio antes de que la imagen se descargue, evitando saltos de layout que penalizan el CLS.
<!-- ❌ Sin dimensiones: el layout salta cuando carga la imagen -->
<img src="photo.jpg" alt="Foto" />
<!-- ✅ Con dimensiones: el espacio se reserva desde el inicio -->
<img src="photo.jpg" alt="Foto" width="800" height="600" />
No hace falta que coincidan con el tamaño de visualización final (el CSS controla eso), lo importante es que la relación de aspecto sea correcta para que el navegador calcule bien el espacio reservado.
decoding
Indica al navegador si puede decodificar la imagen de forma asíncrona sin bloquear el hilo principal.
<!-- Para imágenes no críticas: no bloquea el renderizado -->
<img src="photo.jpg" alt="Foto" decoding="async" />
<!-- Para la imagen LCP: fuerza decodificación síncrona -->
<img src="hero.jpg" alt="Hero" decoding="sync" />
decoding="async" es una buena opción por defecto para imágenes secundarias. Para la imagen del LCP, decoding="sync" asegura que se muestra lo antes posible una vez descargada.
fetchpriority
Permite ajustar la prioridad de descarga dentro del sistema de prioridades del navegador.
<!-- ❌ El navegador puede no priorizarla correctamente -->
<img src="hero.jpg" alt="Hero" />
<!-- ✅ Señala explícitamente que es crítica -->
<img src="hero.jpg" alt="Hero" fetchpriority="high" />
<!-- Para imágenes no críticas que podrían descargarse antes de lo necesario -->
<img src="banner.jpg" alt="Banner" fetchpriority="low" />
fetchpriority="high" es especialmente útil cuando la imagen LCP está dentro de un carrusel o aparece tarde en el HTML, y el navegador no la detecta como prioritaria por sí solo.
La combinación óptima para el LCP
Para la imagen que determina el LCP, hay dos frentes: la descarga y el renderizado.
fetchpriority="high" eleva la prioridad de descarga, pero actúa cuando el parser ya ha encontrado la imagen en el HTML. Si la imagen aparece tarde en el documento —dentro de un componente, un carrusel o con CSS de fondo—, el navegador puede descubrirla demasiado tarde.
El <link rel="preload"> en el <head> resuelve esto: inicia la descarga antes de que el parser llegue a la imagen.
<!-- En el <head>: inicia la descarga lo antes posible -->
<link rel="preload" as="image" href="hero.jpg" fetchpriority="high" />
<!-- En el <body>: con los atributos de renderizado correctos -->
<img
src="hero.jpg"
alt="Hero"
width="1200"
height="600"
loading="eager"
decoding="sync"
fetchpriority="high"
/>
Si usamos srcset con formatos modernos, el preload admite imagesrcset e imagesizes para que el navegador precargue exactamente el recurso que va a usar:
<link
rel="preload"
as="image"
imagesrcset="hero-400.avif 400w, hero-800.avif 800w, hero-1600.avif 1600w"
imagesizes="(max-width: 600px) 100vw, 800px"
fetchpriority="high"
/>
Y si además usamos <picture> con formatos modernos y srcset con sizes, tenemos la imagen bien servida en todos los frentes.
CDN de imágenes: la solución pragmática
Mantener todas estas variantes de formato y tamaño de forma manual es costoso. Los CDN de imágenes como Cloudinary, Imgix o Cloudflare Images automatizan la parte más pesada: detectan el formato óptimo para cada navegador mediante el header Accept, generan los tamaños necesarios bajo demanda y sirven las imágenes optimizadas desde la ubicación más cercana.
Con Cloudinary, por ejemplo, basta con declarar parámetros en la URL:
<img
src="https://res.cloudinary.com/demo/image/upload/f_auto,q_auto,w_800/photo.jpg"
width="800"
height="600"
alt="Foto"
loading="lazy"
decoding="async"
/>
f_auto selecciona AVIF, WebP o JPEG según el navegador. q_auto ajusta la calidad automáticamente. Todo sin gestionar múltiples archivos.
Conclusión
El elemento <img> tiene mucho más potencial del que solemos aprovechar. Con srcset y sizes servimos el tamaño correcto; con <picture> y formatos modernos reducimos el peso; con width y height evitamos saltos de layout; con loading, decoding y fetchpriority controlamos cuándo y cómo se cargan. Y para la imagen LCP, añadir un <link rel="preload"> puede ser la diferencia entre un LCP en verde o en rojo.
Si queréis profundizar en cada uno de estos atributos con más ejemplos, he recopilado las mejores prácticas en el repositorio image-element.
Para auditar el estado de las imágenes en cualquier página, podéis usar el snippet Image Element Audit de WebPerf Snippets: lo ejecutáis directamente en la consola de DevTools y os muestra un análisis de los atributos de cada
<img>de la página.