API Intersection Observer de JavaScript
Aprende la API Intersection Observer de JavaScript para detectar eficientemente cuándo un elemento entra o sale del viewport, para carga diferida, scroll infinito y animaciones activadas por scroll.
La API Intersection Observer permite pedirle al navegador que te avise cuando un elemento entra o sale de la parte visible de la página. Lo hace de forma eficiente y asíncrona, sin el costo de rendimiento que conlleva escuchar eventos de scroll por tu cuenta. Es la herramienta adecuada para la carga diferida de imágenes, la construcción de scroll infinito, activar animaciones a medida que el contenido aparece, y medir si un anuncio o banner fue realmente visto.
El Problema que Resuelve
Antes de que existiera esta API, responder la pregunta simple "¿está este elemento en pantalla ahora mismo?" era sorprendentemente doloroso. Tenías que añadir un listener al evento scroll (y frecuentemente también resize), y luego llamar a getBoundingClientRect() en cada elemento rastreado para comparar su posición con el viewport.
// The old, expensive way — runs on every scroll tick.
window.addEventListener('scroll', () => {
const rect = element.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
if (inView) {
// do something
}
});Los eventos de scroll se disparan docenas de veces por segundo, y getBoundingClientRect() obliga al navegador a recalcular el layout (un "reflow"). Realizar ese trabajo de forma síncrona en el hilo principal durante un scroll es una fuente clásica de jank. (Consulta Manejo de Eventos en el DOM y Scroll en JavaScript para ver cómo se comportan estos eventos.)
IntersectionObserver invierte el modelo. En lugar de que tú consultes las posiciones, el navegador vigila los elementos por ti y solo llama de vuelta cuando la visibilidad realmente cambia. El trabajo ocurre fuera del hilo principal, por lo que no bloquea el scroll. Para más información sobre por qué esto importa, lee Optimización del Rendimiento del DOM. Es un hermano cercano de la API MutationObserver, que observa cambios en la estructura del DOM en lugar de la visibilidad.
Uso Básico
Creas un observer con un callback y luego le indicas qué elementos vigilar con observe().
// 1. Create an observer with a callback and (optional) options.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('Element is now visible:', entry.target);
} else {
console.log('Element left the viewport:', entry.target);
}
});
});
// 2. Start watching a target element.
const target = document.querySelector('#box');
observer.observe(target);El callback recibe un array de entradas, una por cada elemento observado cuya visibilidad cambió. Un único observer puede vigilar muchos elementos, y ese es el patrón recomendado: crea un observer y llama a observe() para cada objetivo en lugar de crear un observer por elemento.
El callback se ejecuta de forma asíncrona y los cambios se agrupan en lote — el navegador puede reportar varias entradas en una sola llamada. También se dispara una vez justo después de que empiezas a observar, de modo que obtienes el estado de visibilidad inicial del elemento sin esperar a un scroll. El soporte en navegadores es excelente en todos los navegadores modernos.
Configurar el Observer
El segundo argumento del constructor es un object de opciones con tres propiedades.
root
El elemento usado como viewport para verificar la visibilidad. El objetivo debe ser descendiente del root. Cuando root es null (el valor predeterminado), se usa el propio viewport del navegador.
const observer = new IntersectionObserver(callback, {
root: document.querySelector('#scroll-container'),
});rootMargin
Un margen alrededor del root, escrito como un valor margin de CSS. Agranda o reduce el cuadro usado para las verificaciones de intersección. Un truco habitual es un margen inferior positivo para que los elementos se reporten como "visibles" antes de desplazarse realmente al viewport — útil para cargar contenido con anticipación.
const observer = new IntersectionObserver(callback, {
// Trigger 200px before the element reaches the bottom edge.
rootMargin: '0px 0px 200px 0px',
});threshold
Un número de 0 a 1, o un array de números, que indica al observer en qué ratios de visibilidad dispararse. 0 significa "disparar en cuanto sea visible un solo píxel," 1 significa "disparar solo cuando el elemento sea completamente visible." Un array se dispara en cada ratio indicado.
const observer = new IntersectionObserver(callback, {
// Fire at 0%, 50%, and 100% visibility.
threshold: [0, 0.5, 1],
});Qué Contiene una Entrada
Cada object en el array entries describe la visibilidad de un elemento en el momento en que se ejecutó el callback. Las propiedades más útiles son:
isIntersecting— un boolean:truesi el elemento es actualmente visible dentro del root.intersectionRatio— qué parte del elemento es visible, de0a1.target— el elemento que se está observando.boundingClientRect— el tamaño y la posición del objetivo.intersectionRect— la porción visible del objetivo.rootBounds— el rectángulo del root (ajustado porrootMargin).time— una marca de tiempo de cuándo se registró el cambio.
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
console.log(entry.target.id, 'visible:', entry.isIntersecting);
console.log('ratio:', entry.intersectionRatio.toFixed(2));
}
});Métodos: observe, unobserve, disconnect
Una instancia de observer te proporciona tres métodos:
observe(element)— empieza a vigilar un elemento.unobserve(element)— deja de vigilar un elemento.disconnect()— deja de vigilar todos los elementos a la vez.
Una buena práctica clave: una vez que un elemento ha completado su tarea única — por ejemplo, una imagen que ha terminado de cargarse de forma diferida — llama a unobserve() sobre él para que el navegador deje de rastrear algo que nunca volverá a cambiar.
Caso de Uso 1 — Carga Diferida de Imágenes
La carga diferida retrasa las descargas de imágenes hasta que estén a punto de ser vistas. Pon la URL real en un atributo data-src, observa cada imagen y cámbiala en src cuando se vuelva visible — luego deja de observarla.
<img data-src="photo-1.jpg" alt="First photo" width="600" height="400" />
<img data-src="photo-2.jpg" alt="Second photo" width="600" height="400" />
<img data-src="photo-3.jpg" alt="Third photo" width="600" height="400" />const images = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src; // load the real image
img.removeAttribute('data-src');
observer.unobserve(img); // job done — stop watching it
});
}, { rootMargin: '0px 0px 200px 0px' }); // start loading a little early
images.forEach((img) => imageObserver.observe(img));Los navegadores modernos también admiten el atributo nativo loading="lazy" en <img> e <iframe>, que no necesita JavaScript en absoluto. Recurre a IntersectionObserver cuando necesites un comportamiento personalizado — un intercambio de marcador de posición, un fundido de entrada o carga de contenido que no sea una imagen.
Caso de Uso 2 — Scroll Infinito
Para el scroll infinito, coloca un elemento "centinela" vacío al final de la lista. Cuando ese centinela se desplace al viewport, carga la siguiente página de datos y añádela. Como el centinela permanece al final, el mismo observer sigue disparándose a medida que el usuario hace scroll.
<ul id="list"></ul>
<div id="sentinel"></div>const list = document.querySelector('#list');
const sentinel = document.querySelector('#sentinel');
let page = 1;
let loading = false;
async function loadMore() {
if (loading) return; // guard against overlapping loads
loading = true;
const res = await fetch('/api/items?page=' + page);
const items = await res.json();
items.forEach((item) => {
const li = document.createElement('li');
li.textContent = item.title;
list.appendChild(li);
});
page += 1;
loading = false;
}
const scrollObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
});
scrollObserver.observe(sentinel);Caso de Uso 3 — Animaciones al Hacer Scroll
Un efecto popular es desvanecer o deslizar elementos al entrar al viewport. Mantén la animación en CSS y deja que JavaScript añada una clase en el momento adecuado.
.reveal {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}const revealItems = document.querySelectorAll('.reveal');
const revealObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target); // animate only once
}
});
}, { threshold: 0.15 }); // fire when ~15% is showing
revealItems.forEach((el) => revealObserver.observe(el));Caso de Uso 4 — Seguimiento de Impresiones / Visibilidad
Los análisis frecuentemente necesitan saber si el contenido fue realmente visto, no solo si está presente en el DOM. Un threshold más alto permite registrar una impresión solo cuando una porción significativa de un elemento es visible.
const adObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio >= 0.5) {
sendImpression(entry.target.dataset.adId);
adObserver.unobserve(entry.target); // count each ad once
}
});
}, { threshold: 0.5 }); // at least 50% visible
document.querySelectorAll('.ad').forEach((ad) => adObserver.observe(ad));Podrías ampliar esto con un temporizador para requerir, por ejemplo, un segundo completo de visibilidad al 50% antes de contar una impresión — un estándar habitual para anuncios "visibles".
Resumen
La API Intersection Observer reemplaza los listeners de scroll frágiles y hambrientos de rendimiento con una forma limpia y asíncrona de reaccionar a la visibilidad de los elementos. Crea un observer, apúntalo a tus objetivos con observe(), lee isIntersecting e intersectionRatio en el callback, y llama a unobserve() una vez que el trabajo de un elemento haya terminado. Con root, rootMargin y threshold puedes ajustar exactamente cuándo se dispara — haciendo que la carga diferida, el scroll infinito, las animaciones de scroll y el seguimiento de impresiones sean simples y fluidos.