PRIVACIDAD

Tus preferencias.

Rechazar no limita el acceso a la web. Cerrar sin guardar conserva tu decisión anterior.

Técnicas · siempre activas

Guardamos únicamente tu elección durante un máximo de 12 meses en este navegador.

Consultar proveedores, duración y transferencias

WPO

Lazy loading y carga diferida: guía SEO sin perder LCP

Qué diferir, qué no tocar nunca y qué documenta Google sobre rastreo, con una prueba de peticiones y LCP medida en mi propia web con Playwright.

¿Quieres aplicarlo a tu web?Hablemos
ResumenQué se puede diferir y con qué mecanismo
Puntos clave
  • El loading="lazy" nativo en <img> es el punto de partida: cero JavaScript, soporte amplio y la URL de la imagen sigue en el HTML. Se rompe cuando lo aplicas a lo que se ve al abrir la página.
  • En una prueba de laboratorio con una sola imagen principal, diferirla subió el LCP de 320 ms a 548 ms. La imagen LCP va sin lazy, y si es candidata clara, con fetchpriority="high".
  • Google documenta como válidos el lazy loading nativo, IntersectionObserver y las librerías que cargan al entrar en el viewport, porque Google Search no hace scroll ni clics. Lo que depende de una interacción no se carga.
  • Con JavaScript desactivado, loading="lazy" no difiere nada: en mi prueba se pidieron las 80 imágenes de golpe. Está documentado por MDN como medida anti-rastreo del usuario.
  • La carga diferida de JavaScript es otra familia: defer, async, módulos e import(). Aquí el riesgo SEO es dejar contenido que depende de un script que llega tarde.
  • En mi web, un artículo de 18 imágenes pide 6 al cargar (191 KB) en vez de 16 (640 KB) en escritorio, y 3 en lugar de 15 en móvil. Lo medí con Playwright quitando el atributo.

Hay una forma muy rápida de arruinar el LCP de una web sin tocar el servidor ni el diseño: que un plugin añada loading="lazy" a todas las imágenes, incluida la que ocupa media pantalla al abrir la página. El navegador deja de pedirla en cuanto la descubre y espera a que el layout confirme que está en el viewport.

El mismo mecanismo, bien aplicado, evita descargar cientos de kilobytes que nadie va a ver. Cubro las imágenes, el criterio de qué no diferir, el umbral de distancia que medí, IntersectionObserver para lo personalizado, qué documenta Google sobre rastreo, la carga diferida de JavaScript y de bloques de página, los errores que más veo y una medición con Playwright sobre esta misma web. Los iframes tienen su propio artículo y aquí solo los toco para enlazarlo.

Cinco tipos de recurso que se pueden diferir: imágenes, vídeo e iframes, JavaScript, componentes y bloques de contenido, con el mecanismo de cada uno
Cinco cosas que se pueden diferir. El mecanismo cambia; la regla de no diferir lo que se ve al abrir la página, no.

Qué se puede diferir y con qué mecanismo

«Carga diferida» agrupa técnicas que no tienen nada en común salvo la idea: no hacer ahora lo que se puede hacer luego. Separarlas evita el error más común, que es usar la herramienta de una familia para un problema de otra.

Qué difieresMecanismoRiesgo principal
Imágenesloading="lazy" en <img>Diferir la imagen LCP o las que se ven al abrir la página
Iframes y embedsloading="lazy" en <iframe>; facadeContenido solo tras un clic. Lo trato en el artículo sobre lazy loading en iframes
Imágenes y fondos personalizadosIntersectionObserverURL en data-src sin src ni noscript
JavaScriptdefer, async, type="module", import()Contenido que depende de un script que llega tarde
Bloques de la páginacontent-visibility: auto, componentes montados al entrar en vistaAlturas inestables que provocan CLS

loading="lazy" en imágenes: qué hace y quién lo soporta

MDN define dos valores. eager, el valor por defecto, carga la imagen de inmediato aunque esté fuera del viewport. lazy «difiere la carga hasta que alcanza una distancia calculada desde el viewport, definida por el navegador».

web.dev da el soporte mínimo: Chrome 77, Edge 79, Firefox 75 y Safari 15.4. Un navegador que no lo conoce lo ignora y carga la imagen sin más.

<img src="/img/grafico.webp" width="800" height="450"
     loading="lazy" decoding="async"
     alt="Evolución de clics orgánicos por mes">

Hay un matiz que casi nadie recuerda y que puedes comprobar: según MDN, la carga solo se difiere si JavaScript está habilitado. El motivo es de privacidad, porque sin esa condición una web podría deducir hasta dónde hace scroll una persona contando qué imágenes se piden y cuándo. Lo probé con 80 imágenes lazy en una página de prueba: con JavaScript activado, Chromium pidió 19; con JavaScript desactivado, pidió las 80.

La consecuencia práctica es que el lazy nativo no es un seguro de peso para quien navega sin scripts, y que sigue siendo un <img src> normal en el HTML: no hay nada que un rastreador tenga que ejecutar para ver la URL. Para el peso, formatos y srcset, la guía está en optimizar las imágenes para SEO.

A qué distancia del viewport empieza a cargar

web.dev documenta que en Chrome, desde julio de 2020, el umbral baja de 3000 px a 1250 px en conexiones rápidas (4G) y de 4000 px a 2500 px en lentas (3G o inferior), y que esos valores están fijos en el código. MDN, por su parte, solo dice que la distancia la calcula el navegador.

Esquema de una página con el viewport arriba y una zona de unos 3000 píxeles por debajo donde Chromium 141 pidió las imágenes diferidas en mi prueba
Lo que medí en Chromium 141: las imágenes cuyo borde superior quedaba a unos 3000 px por debajo del viewport se pidieron al cargar.

Lo medí con una página de prueba de 80 imágenes loading="lazy" de altura variable (100, 200, 500 y 1000 px) y dos alturas de viewport (500 y 800 px). En todos los casos, la última imagen pedida al cargar tenía su borde superior a entre 2200 y 2900 px por debajo del viewport, lo que encaja con un umbral cercano a 3000 px, no con 1250. El navegador reportaba conexión efectiva 4g; con una emulación de 3G salieron 2400 px, un dato en el que confío menos.

No sé si la diferencia con lo documentado se debe a la versión de Chromium, al modo headless o a otra cosa, y no lo he contrastado con el código de Chromium. Como decisión de diseño la conclusión es la misma: el umbral lo pone el navegador, cambia entre versiones y no debes depender de un número. Si necesitas control exacto, la herramienta es IntersectionObserver con tu propio rootMargin.

Qué no diferir: la imagen LCP y lo que se ve al abrir

La guía de optimización del LCP de web.dev lo dice sin matices: nunca apliques lazy loading a la imagen LCP. El motivo es mecánico. Con lazy, el navegador no la pide cuando la descubre en el HTML, sino cuando el layout ha confirmado que cae dentro de la distancia. Eso exige tener el CSS y haber calculado la página, y esa espera se suma al LCP.

Gráfico de barras con la mediana de LCP de una prueba de laboratorio: 548 ms con loading lazy, 320 ms con eager y 308 ms con eager y fetchpriority high
Prueba de laboratorio con una imagen principal, hoja de estilos con 250 ms de retraso e imágenes con 150 ms. Mediana de siete cargas por variante.

Para verlo monté una página con una imagen principal de 1600 × 900, una hoja de estilos que tarda 250 ms y seis imágenes secundarias, servida desde un servidor local con latencia artificial. Siete cargas por variante, Chromium 141:

Variante de la imagen principalInicio de su petición (mediana)LCP (mediana)
loading="lazy"279 ms548 ms
loading="eager"13 ms320 ms
eager + fetchpriority="high"10 ms308 ms

La petición de la variante lazy arrancó justo después de que llegara la hoja de estilos (unos 260 ms), en lugar de salir con el HTML. Es un laboratorio con latencias inventadas, no una medición de campo: sirve para ver el mecanismo, no para prometer 230 ms de mejora en tu web. Fuera del laboratorio, web.dev publica datos de HTTP Archive: el 29 % de los sitios usa lazy nativo en imágenes, y el LCP en el percentil 75 es de 2.922 ms sin lazy y 3.546 ms con él. Los propios autores avisan de que es una correlación y no prueba causalidad.

Lo que sí sale de la documentación es la regla: loading="lazy" solo para imágenes fuera del viewport inicial. En la práctica:

  • La imagen LCP, sin loading="lazy". Si es candidata clara, añade fetchpriority="high": en el artículo sobre cuándo subir la prioridad de una imagen lo desarrollo con sus límites.
  • Las primeras imágenes que se ven al abrir la página, en escritorio y en móvil. El pliegue cambia de sitio entre dispositivos y la comprobación se hace en ambos.
  • Una imagen LCP que no está en el HTML (fondo CSS o inyectada por JavaScript) no se arregla con quitar lazy; ahí entra precargar un recurso con prioridad alta.

El marco de métricas, con los umbrales de LCP y CLS, está en la guía de Core Web Vitals.

Dimensiones, CLS y decoding

Una imagen sin cargar mide 0 × 0 píxeles si no declaras tamaño. MDN lo explica con un efecto menos obvio que el salto de layout: como las imágenes diferidas no se cargan si no intersectan una parte visible, y una imagen descargada a 0 × 0 no intersecta nada, declarar width y height es «especialmente importante» con lazy. web.dev añade el caso inverso: en una galería sin dimensiones, el navegador puede cargarlas todas porque caben a la vez en la vista inicial.

Con atributos de tamaño y height: auto en CSS, el navegador deduce la proporción y reserva el hueco antes de descargar el archivo.

decoding="async" no es lazy loading

decoding acepta sync, async y auto (el valor por defecto). Con async, el siguiente pintado no espera a decodificar la imagen. MDN reconoce que en un <img> estático es difícil percibir el efecto, aunque el bloqueo se puede medir, y que se nota más al insertar imágenes dinámicamente. Va bien combinado con lazy, pero no reduce las peticiones: solo cambia cuándo se decodifica.

IntersectionObserver: cuando el nativo no te llega

MDN describe IntersectionObserver como una API que avisa de forma asíncrona cuando un elemento entra o sale de la intersección con un ancestro o con el viewport. Tiene soporte Baseline «widely available» y está en todos los navegadores desde marzo de 2019. Sirve para lo que loading no cubre: fondos CSS, imágenes que cambian de fuente, componentes que quieres montar al acercarse o un umbral propio con rootMargin.

<img data-src="/img/grafico.webp" src="/img/placeholder.svg"
     width="800" height="450" alt="Evolución de clics por mes">
<noscript><img src="/img/grafico.webp" width="800" height="450"
     alt="Evolución de clics por mes"></noscript>

<script>
const io = new IntersectionObserver((entries, obs) => {
  for (const e of entries) {
    if (!e.isIntersecting) continue;
    e.target.src = e.target.dataset.src;
    obs.unobserve(e.target);
  }
}, { rootMargin: '400px 0px' });
document.querySelectorAll('img[data-src]').forEach(i => io.observe(i));
</script>

El patrón funciona, pero cada pieza es una oportunidad de error. Si el HTML inicial solo trae data-src, la imagen no tiene src hasta que se ejecuta tu script. Google renderiza JavaScript, pero tu código pasa a ser la única garantía de que la URL acabe en el src. Con noscript cubres a quien navega sin scripts. Mi criterio: si loading="lazy" resuelve el caso, uso el nativo; el observador solo cuando necesito algo que el atributo no hace.

Qué dice Google sobre lazy loading y rastreo

La página de Google Search Central sobre contenido con carga diferida empieza con una advertencia: mal implementada, esta técnica «puede ocultar contenido a Google sin querer». Y fija el criterio central: Google Search no interactúa con tu página, así que los métodos válidos no deben depender de acciones del usuario como hacer scroll o clic. El contenido tiene que cargarse al entrar en el viewport.

Dos columnas: métodos de carga diferida que Google documenta como compatibles (nativo, IntersectionObserver, librería) y cargas que dependen de scroll o clic
Qué documenta Google como compatible y qué queda fuera.

Los métodos que recomienda son tres: el lazy loading nativo del navegador para imágenes e iframes, IntersectionObserver (con su polyfill) y una librería de JavaScript que cargue al entrar en el viewport. Tampoco quiere ver el diferido aplicado a contenido que probablemente se vea al abrir la página, porque tarda más en mostrarse, algo «muy visible para el usuario».

Scroll infinito y paginación

Si cargas más resultados al hacer scroll, Google pide que cada fragmento tenga una URL propia y persistente, con números de página absolutos (?page=12) y no relativos como ?date=yesterday, que las páginas se enlacen entre sí en secuencia y que, al llegar a un nuevo fragmento, la URL se actualice con la History API. Un listado que solo existe tras scroll es, para Google, un listado de una sola página.

Cómo comprobar lo que ve Google

La propia guía recomienda la Inspección de URL de Search Console: si las URLs de imágenes o vídeos aparecen en el atributo src de los <img> o <video> del HTML renderizado, la implementación es correcta. Es la misma lógica con la que Google procesa cualquier página con JavaScript, que explico en cómo renderiza Google el JavaScript.

Diferir JavaScript: defer, async, módulos e import()

Aquí no hay viewport. Se difiere cuándo se descarga o ejecuta un script, y el coste SEO aparece cuando el contenido de la página depende de él.

Línea de tiempo del análisis del HTML con cuatro formas de cargar un script: normal, async, defer e import dinámico
Cuándo se ejecuta cada forma de cargar un script respecto al análisis del HTML.
FormaDescargaEjecución según MDN
<script src> sin atributosBloquea el análisisInmediata, en orden
asyncEn paralelo al análisisEn cuanto esté disponible; sin garantía de orden
deferEn paralelo al análisisTras analizar el documento y antes de DOMContentLoaded, en orden de aparición
type="module"En paraleloDiferido por defecto; defer no tiene efecto
import()Cuando lo llamasDevuelve una promesa con el módulo

Dos detalles de MDN evitan fallos: async y defer no tienen efecto en scripts inline clásicos (sin src), y un script con ambos se comporta como async. El import() dinámico solo conviene, dice la propia documentación, cuando hace falta: el import estático es preferible para las dependencias iniciales por el análisis estático y el tree shaking. Lo uso para código que se ejecuta tras un clic, no para el que pinta el contenido principal.

Bloques de página: content-visibility

Para secciones largas hay una vía sin JavaScript: content-visibility: auto permite al navegador saltarse el layout y el pintado de lo que está fuera de pantalla. web.dev cita una demo con un 7× de mejora en la carga inicial y un render que pasa de 232 ms a 30 ms. Necesita contain-intrinsic-size para no alterar el scroll, y el contenido sigue en el DOM y en el árbol de accesibilidad, por lo que se puede buscar con Ctrl+F. Soporte según web.dev: Chrome 85, Edge 85, Firefox 125 y Safari 18. No lo aplicaría a lo que se ve al abrir la página.

Errores típicos que encuentro

Seis errores frecuentes de carga diferida: lazy en la imagen principal, data-src sin fallback, sin dimensiones, plugin que lo aplica a todo, contenido tras scroll o clic y probar solo en escritorio
Seis fallos que se repiten en auditorías.
ErrorConsecuenciaArreglo
lazy en la imagen principalLa petición espera al layout y sube el LCPQuitarlo; fetchpriority="high" si procede
Plugin o tema que lo añade a todas las imágenesMismo efecto, sin que nadie lo haya decididoRevisar el HTML final, no los ajustes del plugin
data-src sin src ni noscriptLa URL de la imagen no está en el HTML inicialUsar el nativo o añadir fallback
Sin width y heightSalto de layout; en galerías, todo se carga a la vezDeclarar dimensiones o aspect-ratio
Contenido que aparece solo tras scroll o clicGoogle Search no interactúa con la páginaCarga al entrar en el viewport
Probar solo en escritorioEn móvil el pliegue está en otro sitioComprobar ambos viewports

Si el plugin o el tema añade lazy a todo, no hace falta ir imagen a imagen: casi todos permiten excluir las primeras imágenes de la página o la imagen destacada. Revisa el HTML final después, no solo el ajuste.

Cómo lo tengo en mi web

Esto es lo que hay hoy en cristofercruz.net, leído en las plantillas y medido en 127.0.0.1:8099 con Playwright y Chromium 141. Primero, el código:

  • La portada de cada artículo lleva fetchpriority="high" y no lleva loading, de modo que carga como eager. El logo de la cabecera y el avatar de la firma tampoco llevan loading.
  • El resto va con loading="lazy" y decoding="async": figuras del cuerpo, avatar y tarjeta del final, tarjetas de artículos relacionados, el logo del pie y la flecha del menú de servicios.
  • En la home, el retrato principal va con fetchpriority="high" y además tiene un <link rel="preload" as="image"> con imagesrcset. En las páginas de ciudad y servicios, la imagen principal lleva loading="eager" y fetchpriority="high".
  • No uso data-src ni librerías de lazy loading. El único IntersectionObserver del sitio está en post.js y resalta la sección activa del índice del artículo.
  • Los tres scripts propios (cookie-consent.js, post.js, site.js) llevan defer. Los scripts de analítica y publicidad viven en plantillas inertes y solo se activan tras el consentimiento: eso lo leo en el código, no lo he medido.
  • No hay content-visibility en el CSS ni <video> o <iframe> en las plantillas.
Comparación de peticiones de imagen al cargar un artículo de mi web con loading lazy y sin él, en escritorio y en móvil
Peticiones de imagen al cargar /blog/cache-seo/, con el atributo y con el atributo eliminado en la respuesta.

Después, la prueba. Cargué tres artículos sin y con loading="lazy" (quitando el atributo con una ruta de Playwright que reescribe el HTML) en un viewport de escritorio de 1280 × 800 y otro móvil de 390 × 844, con los dominios externos bloqueados. Cuento las peticiones de imagen al terminar la carga y su peso por la cabecera content-length:

Artículo (etiquetas img)ViewportCon lazy: peticiones / pesoSin lazy: peticiones / peso
cache-seo (18, 15 lazy)Escritorio6 / 191 KB16 / 640 KB
cache-seoMóvil3 / 77 KB15 / 614 KB
optimizacion-imagenes-seo (18, 15 lazy)Escritorio6 / 210 KB16 / 671 KB
core-web-vitals (29, 26 lazy)Escritorio6 / 213 KB27 / 1,33 MB
core-web-vitalsMóvil4 / 137 KB26 / 1,30 MB

En escritorio, tras recorrer la página entera, el total sube a 15 peticiones y unos 616 KB: el lazy no ahorra el peso total si el usuario llega al final, retrasa el momento en que se paga. También hay una imagen lazy que no se descarga nunca, la flecha del menú de servicios, porque está en un desplegable oculto.

Un hallazgo que no esperaba: en pantallas de hasta 900 px, la portada del artículo se sustituye por un GIF de 1 píxel mediante un <source media="(max-width:900px)">. Lo vi porque en móvil la petición de la portada no aparecía y currentSrc devolvía un data:image/gif. Es una decisión de diseño, no un fallo: en ese ancho el CSS oculta el contenedor de la portada (.post-hero-media{display:none}). Afecta a cómo leer la medición móvil: el fetchpriority="high" de la portada sigue en el HTML, aunque en ese viewport apunte a un píxel. Para el resto de criterios móviles, mira el artículo sobre SEO para la versión móvil.

Cómo auditar la carga diferida

Cuatro pasos de auditoría: inventario en la consola, pestaña Network con y sin scroll, LCP y CLS en móvil e Inspección de URL
Cuatro comprobaciones, de la más rápida a la más cercana a lo que ve Google.
  1. Inventario. En la consola de DevTools, lista cada imagen con su atributo, su tamaño declarado y su posición:
    [...document.images].map(i => ({
      src: i.currentSrc.split('/').pop(),
      loading: i.loading,
      fp: i.getAttribute('fetchpriority'),
      w: i.getAttribute('width'),
      top: Math.round(i.getBoundingClientRect().top + scrollY)
    }))
  2. Network, con y sin scroll. Filtra por Img, recarga sin tocar la página y mira qué se pide. Después haz scroll despacio. Una imagen visible al abrir que sale tarde es candidata a quitarle el lazy.
  3. LCP y CLS en móvil. Identifica el elemento LCP en el panel de rendimiento o en PageSpeed y comprueba que no lleva loading="lazy". Repite con CPU y red limitadas.
  4. HTML renderizado. En la Inspección de URL de Search Console, abre el HTML renderizado y busca las imágenes diferidas: la URL debe estar en src.

Para desactivar el atributo sin tocar el código y comparar, la misma técnica de ruta con Playwright que usé arriba reescribe la respuesta antes de que el navegador la vea. Si quieres que lo revise en tu sitio con todas las plantillas, es parte de lo que cubre una auditoría técnica de SEO.

Auditoría SEO

Ver auditoría SEO

Preguntas frecuentes

¿El lazy loading perjudica al SEO?

No por sí mismo. Google documenta el lazy loading nativo, IntersectionObserver y las librerías que cargan al entrar en el viewport como métodos compatibles. Lo que perjudica es aplicarlo a la imagen LCP o a contenido que solo carga tras un scroll o un clic.

¿Puedo poner loading="lazy" a todas las imágenes?

No. Las que se ven al abrir la página, y sobre todo la imagen LCP, deben cargar sin diferido. web.dev lo resume así: usa lazy solo en imágenes fuera del viewport inicial.

¿Ve Googlebot las imágenes con loading="lazy"?

Google indica que, si las URLs de las imágenes aparecen en el src del HTML renderizado, la implementación es correcta. Compruébalo en la Inspección de URL.

¿Necesito noscript con el lazy nativo?

No: la imagen sigue siendo un <img src> normal. El noscript hace falta con patrones que ponen la URL en data-src. Y recuerda que con JavaScript desactivado el nativo no difiere nada.

¿A cuántos píxeles empieza a cargar una imagen lazy?

Lo decide el navegador. web.dev documentó 1250 px en 4G y 2500 px en 3G para Chrome desde 2020; en mi prueba con Chromium 141 salió en torno a 3000 px. No lo uses como dato de diseño.

¿Es mejor el atributo nativo o una librería?

El nativo, mientras cubra el caso. Una librería o IntersectionObserver tienen sentido para fondos CSS, umbrales propios o componentes, y entonces el fallback es responsabilidad tuya.

¿Cómo se hace un scroll infinito indexable?

Cada fragmento necesita una URL propia y persistente (?page=12), enlazada en secuencia, y la URL debe actualizarse con la History API al llegar a un nuevo fragmento. Es lo que recomienda Google.

Referencias

  1. Fix lazy-loaded content Google Search Central
  2. Browser-level image lazy-loading for the web web.dev
  3. <img>: the Image Embed element MDN
  4. Don't lazy-load your LCP image web.dev
  5. Intersection Observer API MDN
  6. <script>: the Script element MDN
  7. import() (dynamic import) MDN
  8. content-visibility: the new CSS property that boosts your rendering performance web.dev

Sigue leyendo

WPO

· 13 min

Lazy loading en iframes: cómo aplicarlo sin romper el SEO

Cuándo usar loading="lazy" en iframes, qué no diferir nunca, cómo evitar CLS y cuándo conviene una facade para YouTube o Maps. Con la visión de Googlebot.

Leer el artículo: Lazy loading en iframes: cómo aplicarlo sin romper el SEO
Multimedia

· 14 min

Optimización de imágenes SEO: formatos, carga y tamaño

Qué documenta Google sobre imágenes, cómo elegir formato, tamaño y carga sin romper el LCP ni el CLS, y qué hago yo en mi propia web.

Leer el artículo: Optimización de imágenes SEO: formatos, carga y tamaño

Tu próximo paso empieza con una conversación.

Cuéntame qué necesitas conseguir con tu web.

Hablemos de tu proyecto