</> HTML5Advent
ENFRESDEITPT

// apis · Web Platform Advent #10

La API IntersectionObserver: lazy-loading y efectos al hacer scroll

Usa IntersectionObserver para cargar imágenes de forma diferida, revelar elementos al hacer scroll y crear scroll infinito — con threshold y rootMargin explicados y ejemplos ejecutables.

Vista cenital del escritorio de un diseñador con bocetos de maquetas de interfaz, notas adhesivas, un smartphone y rotuladores de colores

La API IntersectionObserver te indica cuándo un elemento entra o sale del área visible — sin tener que conectar un escuchador scroll y calcular posiciones en cada fotograma. El navegador hace la vigilancia de forma asíncrona y fuera del hilo principal, así que se mantiene fluido incluso con cientos de objetivos. Todos los navegadores actuales la admiten.

¿Por qué no escuchar simplemente el scroll?

Un manejador scroll se dispara constantemente y te obliga a llamar a getBoundingClientRect() para saber dónde está cada cosa — lo que lee el layout y puede provocar tirones. IntersectionObserver invierte el modelo: registras los elementos que te interesan y el navegador solo te avisa cuando su visibilidad cambia de verdad.

El patrón básico

Crea un observador con una función de devolución y luego llama a observe() sobre uno o varios elementos. La devolución recibe un array de objetos entry; la propiedad clave es entry.isIntersecting:

const observer = new IntersectionObserver((entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log('Visible ahora:', entry.target);
    }
  }
});

document.querySelectorAll('.watch').forEach((el) => observer.observe(el));

Carga diferida de imágenes (lazy-loading)

Un uso clásico: cargar una imagen solo cuando está a punto de aparecer. Guarda la URL real en data-src, insértala cuando el elemento intersecte y deja de observarlo:

const lazyImages = document.querySelectorAll('img[data-src]');

const imgObserver = new IntersectionObserver((entries, observer) => {
  for (const entry of entries) {
    if (!entry.isIntersecting) continue;
    const img = entry.target;
    img.src = img.dataset.src;
    observer.unobserve(img); // cargar una vez y detener
  }
}, { rootMargin: '200px' });

lazyImages.forEach((img) => imgObserver.observe(img));

Nota: la carga diferida nativa mediante <img loading="lazy"> cubre ya la mayoría de los casos. Recurre a IntersectionObserver cuando necesites lógica a medida — placeholders, fundidos o cargar recursos que no sean imágenes.

Un smartphone mostrando la pantalla de inicio de sesión de Facebook junto a fichas de Scrabble que forman las palabras SOCIAL MEDIA
Los feeds de scroll infinito como las redes sociales son un caso de manual de IntersectionObserver: se vigila un centinela cerca del final y se carga más contenido cuando aparece.

Revelar elementos al hacer scroll

Añade una clase CSS cuando un elemento se vuelve visible por primera vez, para que pueda animarse. La opción threshold decide qué parte del elemento debe estar visible antes de disparar:

const revealObserver = new IntersectionObserver((entries, observer) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target);
    }
  }
}, { threshold: 0.2 }); // dispara cuando el 20% está en pantalla

document.querySelectorAll('.reveal').forEach((el) => revealObserver.observe(el));

Scroll infinito

Coloca un elemento centinela vacío después de tu lista. Cuando entra en el área visible, obtén y añade la siguiente página, y sigue observando para la siguiente:

const sentinel = document.querySelector('#sentinel');
let page = 1;

const loader = new IntersectionObserver(async (entries) => {
  if (!entries[0].isIntersecting) return;
  const items = await fetchPage(page++);
  appendItems(items);
});

loader.observe(sentinel);

Las opciones: threshold y rootMargin

Dos opciones determinan cuándo se dispara la devolución:

  • threshold — un número (o array) de 0 a 1. 0 dispara en cuanto un píxel es visible; 1 solo cuando todo el elemento lo es. Un array como [0, 0.5, 1] dispara en cada escalón, útil para efectos de progreso al hacer scroll.
  • rootMargin — un margen al estilo CSS que agranda o reduce el área de prueba. '200px' dispara 200px antes de que el elemento alcance el área visible — ideal para precargar.
  • root — el elemento que se usa como área visible. Por defecto, el área del navegador; indica un contenedor con scroll para observar dentro de él.

Limpieza

Deja de vigilar un solo elemento con observer.unobserve(el), o desmonta todo con observer.disconnect() — importante en aplicaciones de una sola página cuando se desmonta una vista, para evitar fugas.

Referencia rápida

ObjetivoOpción / llamada clave
Detectar visibilidadentry.isIntersecting
Precargar con antelaciónrootMargin: '200px'
Esperar X% visiblethreshold: 0.5
Observar dentro de un contenedorroot: containerEl
Detener unoobserver.unobserve(el)
Desmontar todoobserver.disconnect()

IntersectionObserver sustituye toda una categoría de cálculos de scroll frágiles por una pequeña API declarativa. Imágenes diferidas, revelaciones al hacer scroll y listas infinitas se reducen al mismo patrón: observar, reaccionar a isIntersecting y dejar de observar cuando hayas terminado.