En este artículo te contamos cómo aplicamos hoy la carga diferida (lazy load) de imágenes e iframes: qué resuelve ya el navegador con el atributo loading="lazy", qué hace WordPress por defecto y desde qué versión, por qué la imagen principal de la página nunca debe ir en diferido y cuándo sigue teniendo sentido escribir JavaScript con IntersectionObserver. También repasamos los errores típicos: imágenes LCP retrasadas, saltos de maquetación por falta de dimensiones y scripts de lazy load que ya no hacen falta.
Qué es el lazy load y qué problema resuelve
Una página con muchas imágenes o vídeos incrustados descarga, por defecto, todos esos recursos al cargar, aunque el usuario nunca llegue a verlos. Eso compite por ancho de banda con lo que de verdad importa en el primer pantallazo: el HTML, el CSS crítico, las fuentes y la imagen principal.
El lazy load consiste en aplazar la descarga de los recursos que están fuera de la pantalla hasta que el usuario se acerca a ellos con el scroll. Bien aplicado reduce bytes transferidos y deja libre la red para lo importante. Mal aplicado (por ejemplo, sobre la imagen principal) empeora la métrica que más nos interesa vigilar: el LCP (Largest Contentful Paint).
Lazy loading nativo: loading="lazy"
Hoy basta con un atributo HTML:
<img src="/img/equipo.jpg" alt="El equipo trabajando" width="1200" height="800" loading="lazy">
<iframe src="https://www.youtube.com/embed/VIDEO_ID" title="Vídeo de ejemplo" width="560" height="315" loading="lazy"></iframe>
El navegador decide cuándo empezar la descarga en función de la distancia al viewport y del tipo de conexión, así que no tenemos que calcular nada. Algunos detalles que conviene saber:
- En imágenes está soportado en todos los navegadores principales desde que Safari lo incorporó en 2022 (Chrome lo tenía desde la versión 77 y Firefox desde la 75).
- En iframes llegó más tarde: Safari 16.4 y Firefox 121, con lo que es compatible con todos los navegadores actuales desde diciembre de 2023.
- Si el navegador no lo entiende, simplemente ignora el atributo y carga el recurso como siempre. No rompe nada.
- Pon siempre
widthyheight(o unaspect-ratioen CSS). Sin dimensiones, el navegador no reserva espacio y la página salta cuando la imagen llega, lo que penaliza el CLS (Cumulative Layout Shift).
Qué hace WordPress por defecto
Si trabajas con WordPress, gran parte de esto ya lo tienes sin tocar nada:
- WordPress 5.5 empezó a añadir
loading="lazy"a las imágenes del contenido, extractos y widgets de texto, siempre que tenganwidthyheight. También añade esas dimensiones cuando faltan en imágenes de la biblioteca de medios. - WordPress 5.7 extendió el comportamiento a los iframes que tengan ancho y alto, incluidos los embeds (oEmbed) cuyo proveedor devuelva dimensiones.
- WordPress 5.9 dejó de poner lazy en la primera imagen de contenido, para no retrasar la que suele ser la imagen principal.
- WordPress 6.3 afinó esa lógica: tiene en cuenta imágenes de cabecera y anteriores al loop, sube a 3 el número de imágenes iniciales que no se difieren (filtro
wp_omit_loading_attr_threshold) y añade automáticamentefetchpriority="high"a la imagen que considera candidata a LCP. - WordPress 6.7 añade
sizes="auto"a las imágenes conloading="lazy", para que el navegador elija delsrcsetsegún el ancho real de renderizado.
Si tu tema imprime imágenes fuera de las funciones estándar (por ejemplo, con un <img> escrito a mano en una plantilla), WordPress no las toca: ahí te toca a ti decidir qué atributos llevan. En las que sí pasan por wp_get_attachment_image() puedes controlar el atributo:
// Imagen destacada de la cabecera: nunca en diferido y con prioridad alta
echo wp_get_attachment_image( $image_id, 'large', false, array(
'loading' => 'eager',
'fetchpriority' => 'high',
) );
En nuestro caso, este blog es un WordPress headless con Gatsby desplegado en Netlify, así que el HTML final no lo genera WordPress sino el front. Lo mismo aplica: lo que cuenta es el marcado que llega al navegador, y es ahí donde hay que revisar qué imágenes van en diferido y cuáles no.
Por qué la imagen LCP no debe ir en lazy
Un error muy habitual es aplicar lazy load a todas las imágenes, incluida la del hero. El resultado es que el navegador no empieza a descargar la imagen principal hasta que ha calculado el layout y comprobado que está en pantalla, con lo que el LCP se retrasa justo en la imagen que más pesa en la métrica.
La regla práctica:
- Imágenes visibles al cargar (above the fold): sin
loading="lazy", o conloading="eager"de forma explícita. - La imagen LCP (hero, imagen destacada, primera foto de producto): además,
fetchpriority="high". - Todo lo que queda por debajo:
loading="lazy". - Imágenes ocultas en carruseles o pestañas:
fetchpriority="low"si no van a verse de entrada.
<!-- Imagen principal -->
<img src="/img/hero.jpg" alt="Portada del artículo" width="1600" height="900" fetchpriority="high">
<!-- Resto del contenido -->
<img src="/img/detalle.jpg" alt="Detalle del componente" width="800" height="600" loading="lazy">
fetchpriority es una pista para el navegador, no una orden, y conviene usarla con mesura: si marcas cinco imágenes con prioridad alta, ninguna lo es. Está soportado en Chrome desde la versión 102, en Safari desde la 17.2 y en Firefox desde la 132; los navegadores que no lo entienden simplemente lo ignoran.
Para saber cuál es tu imagen LCP, abre el panel Performance de Chrome DevTools o ejecuta PageSpeed Insights: ambos te indican qué elemento es el LCP y si se ha cargado en diferido.
IntersectionObserver: para lo que el nativo no cubre
El atributo loading solo funciona en <img> e <iframe>. Hay casos en los que sigue haciendo falta JavaScript:
- Imágenes de fondo en CSS (
background-image), que el navegador descarga en cuanto aplica la regla. - Componentes pesados: mapas, reproductores, widgets de terceros o bloques que hacen peticiones a una API.
- Animaciones o contadores que solo deben arrancar cuando el usuario llega a ellos.
Para todo esto la herramienta correcta es IntersectionObserver, que avisa cuando un elemento entra (o está a punto de entrar) en el viewport sin tener que escuchar el evento scroll.
Paso 1: fondos CSS en diferido
La idea es no aplicar el fondo hasta que el elemento esté cerca de la pantalla. En el HTML marcamos el bloque:
<section class="banner lazy-bg">
<h2>Contenido del banner</h2>
</section>
En el CSS, el fondo solo se aplica cuando el bloque recibe la clase is-visible:
.banner {
min-height: 400px;
background-color: #e5e5e5; /* color de reserva mientras carga */
}
.banner.lazy-bg.is-visible {
background-image: url("/img/banner.webp");
background-size: cover;
}
Paso 2: el observador
document.addEventListener('DOMContentLoaded', () => {
const targets = document.querySelectorAll('.lazy-bg');
if (!('IntersectionObserver' in window)) {
// Sin soporte: mostramos todo directamente
targets.forEach((el) => el.classList.add('is-visible'));
return;
}
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
obs.unobserve(entry.target);
}
});
}, {
rootMargin: '200px 0px' // empieza a cargar 200px antes de que entre en pantalla
});
targets.forEach((el) => observer.observe(el));
});
Tres detalles que marcan la diferencia respecto al ejemplo que publicamos originalmente: usamos rootMargin para adelantar la carga y que el usuario no vea el hueco, dejamos de observar cada elemento en cuanto se ha cargado y, si el navegador no soporta la API, mostramos el contenido en lugar de dejarlo oculto. El patrón antiguo de poner display: none a las imágenes con clase lazy tenía justo ese riesgo: si el script fallaba, la imagen no aparecía nunca.
Paso 3: cargar un componente solo cuando hace falta
El mismo patrón sirve para importar código bajo demanda. Por ejemplo, un mapa que solo se inicializa al llegar a su sección:
const mapa = document.querySelector('#mapa');
if (mapa) {
const observer = new IntersectionObserver(async ([entry], obs) => {
if (!entry.isIntersecting) return;
obs.disconnect();
const { iniciarMapa } = await import('./mapa.js');
iniciarMapa(mapa);
}, { rootMargin: '300px 0px' });
observer.observe(mapa);
}
Así el JavaScript del mapa no compite con la carga inicial.
Nota sobre las soluciones con jQuery
Hace unos años publicamos una versión de lazy load con jQuery que escuchaba los eventos scroll y load y comparaba la posición de cada imagen con la del viewport. Hoy no la recomendamos: se ejecuta en cada evento de scroll, obliga a cargar jQuery solo para esto y hace a mano lo que el navegador ya resuelve con loading="lazy" o con IntersectionObserver. Si la tienes en un proyecto, lo normal es poder eliminarla sin sustituirla por nada, más allá de revisar los atributos de las imágenes.
Si quieres seguir con el resto de la optimización de imágenes (formatos WebP y AVIF, srcset, compresión y CDN), lo tienes en optimizar imágenes para mejorar el rendimiento en WordPress.