- 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, confetchpriority="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 eimport(). 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.

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é difieres | Mecanismo | Riesgo principal |
|---|---|---|
| Imágenes | loading="lazy" en <img> | Diferir la imagen LCP o las que se ven al abrir la página |
| Iframes y embeds | loading="lazy" en <iframe>; facade | Contenido solo tras un clic. Lo trato en el artículo sobre lazy loading en iframes |
| Imágenes y fondos personalizados | IntersectionObserver | URL en data-src sin src ni noscript |
| JavaScript | defer, async, type="module", import() | Contenido que depende de un script que llega tarde |
| Bloques de la página | content-visibility: auto, componentes montados al entrar en vista | Alturas 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.

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.

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 principal | Inicio de su petición (mediana) | LCP (mediana) |
|---|---|---|
loading="lazy" | 279 ms | 548 ms |
loading="eager" | 13 ms | 320 ms |
eager + fetchpriority="high" | 10 ms | 308 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ñadefetchpriority="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.

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.

| Forma | Descarga | Ejecución según MDN |
|---|---|---|
<script src> sin atributos | Bloquea el análisis | Inmediata, en orden |
async | En paralelo al análisis | En cuanto esté disponible; sin garantía de orden |
defer | En paralelo al análisis | Tras analizar el documento y antes de DOMContentLoaded, en orden de aparición |
type="module" | En paralelo | Diferido por defecto; defer no tiene efecto |
import() | Cuando lo llamas | Devuelve 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

| Error | Consecuencia | Arreglo |
|---|---|---|
lazy en la imagen principal | La petición espera al layout y sube el LCP | Quitarlo; fetchpriority="high" si procede |
| Plugin o tema que lo añade a todas las imágenes | Mismo efecto, sin que nadie lo haya decidido | Revisar el HTML final, no los ajustes del plugin |
data-src sin src ni noscript | La URL de la imagen no está en el HTML inicial | Usar el nativo o añadir fallback |
Sin width y height | Salto de layout; en galerías, todo se carga a la vez | Declarar dimensiones o aspect-ratio |
| Contenido que aparece solo tras scroll o clic | Google Search no interactúa con la página | Carga al entrar en el viewport |
| Probar solo en escritorio | En móvil el pliegue está en otro sitio | Comprobar 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 llevaloading, de modo que carga comoeager. El logo de la cabecera y el avatar de la firma tampoco llevanloading. - El resto va con
loading="lazy"ydecoding="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">conimagesrcset. En las páginas de ciudad y servicios, la imagen principal llevaloading="eager"yfetchpriority="high". - No uso
data-srcni librerías de lazy loading. El único IntersectionObserver del sitio está enpost.jsy resalta la sección activa del índice del artículo. - Los tres scripts propios (
cookie-consent.js,post.js,site.js) llevandefer. 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-visibilityen el CSS ni<video>o<iframe>en las plantillas.

/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) | Viewport | Con lazy: peticiones / peso | Sin lazy: peticiones / peso |
|---|---|---|---|
| cache-seo (18, 15 lazy) | Escritorio | 6 / 191 KB | 16 / 640 KB |
| cache-seo | Móvil | 3 / 77 KB | 15 / 614 KB |
| optimizacion-imagenes-seo (18, 15 lazy) | Escritorio | 6 / 210 KB | 16 / 671 KB |
| core-web-vitals (29, 26 lazy) | Escritorio | 6 / 213 KB | 27 / 1,33 MB |
| core-web-vitals | Móvil | 4 / 137 KB | 26 / 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

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




