Durante años la solución para detectar cuándo un elemento entraba en pantalla o cambiaba de tamaño era escuchar el evento scroll o resize y calcular posiciones con getBoundingClientRect en cada frame. Ese enfoque fuerza al navegador a hacer reflow y puede degradar el rendimiento notablemente. Las APIs Observer del navegador resuelven esto de forma declarativa y eficiente.
IntersectionObserver: visibilidad en el viewport
IntersectionObserver notifica cuando un elemento entra o sale del área visible (el viewport u otro elemento contenedor), sin necesidad de escuchar scroll events. El callback recibe un array de entradas con información sobre el porcentaje de intersección.
const observer = new IntersectionObserver(
(entradas) => {
entradas.forEach((entrada) => {
if (entrada.isIntersecting) {
console.log(`${entrada.target.id} es visible (${Math.round(entrada.intersectionRatio * 100)}%)`);
}
});
},
{
threshold: [0, 0.25, 0.5, 0.75, 1], // notificar en estos porcentajes
rootMargin: '0px 0px -100px 0px', // margen negativo abajo: activar antes de llegar al borde
}
);
document.querySelectorAll('.seccion').forEach(el => observer.observe(el));
Lazy loading de imágenes con IntersectionObserver
El patrón más usado de IntersectionObserver es cargar imágenes solo cuando están a punto de entrar en pantalla, reduciendo las peticiones de red iniciales:
const lazyObserver = new IntersectionObserver(
(entradas, obs) => {
entradas.forEach((entrada) => {
if (!entrada.isIntersecting) return;
const img = entrada.target;
img.src = img.dataset.src; // cargar la imagen real
img.removeAttribute('data-src');
obs.unobserve(img); // dejar de observar tras cargar
});
},
{ rootMargin: '200px' } // empezar a cargar 200px antes de entrar en pantalla
);
document.querySelectorAll('img[data-src]').forEach(img => lazyObserver.observe(img));
Infinite scroll con IntersectionObserver
Detectar cuándo el usuario llega al final de la lista para cargar más resultados es otro uso clásico:
const sentinel = document.getElementById('sentinel'); // elemento vacío al final de la lista
const scrollObserver = new IntersectionObserver(async (entradas) => {
if (!entradas[0].isIntersecting) return;
const nuevos = await cargarMasItems();
if (nuevos.length === 0) {
scrollObserver.disconnect(); // no hay más datos
return;
}
nuevos.forEach(item => lista.appendChild(renderItem(item)));
});
scrollObserver.observe(sentinel);
ResizeObserver: cambios de tamaño de elementos específicos
ResizeObserver notifica cuando el tamaño de un elemento cambia, independientemente de si es por redimensionar la ventana, cambiar el CSS, insertar contenido o animar. A diferencia del evento resize de window, funciona sobre cualquier elemento.
const resizeObserver = new ResizeObserver((entradas) => {
for (const entrada of entradas) {
const { width, height } = entrada.contentRect;
console.log(`Nuevo tamaño de ${entrada.target.id}: ${width}x${height}`);
// Actualizar un gráfico SVG cuando cambia el tamaño del contenedor
if (entrada.target.id === 'chart-container') {
actualizarGrafico(width, height);
}
}
});
resizeObserver.observe(document.getElementById('chart-container'));
resizeObserver.observe(document.getElementById('sidebar'));
// Limpiar cuando ya no se necesite
// resizeObserver.disconnect();
MutationObserver: vigilar el árbol DOM
MutationObserver detecta cambios en el árbol DOM: atributos modificados, nodos añadidos o eliminados, texto cambiado. Es útil para reaccionar a cambios realizados por librerías de terceros o para implementar lógica de componentes sin frameworks.
const mutObserver = new MutationObserver((mutaciones) => {
for (const mut of mutaciones) {
if (mut.type === 'childList') {
mut.addedNodes.forEach(nodo => {
if (nodo.nodeType === 1 && nodo.matches('.tooltip')) {
inicializarTooltip(nodo);
}
});
}
if (mut.type === 'attributes') {
console.log(`Atributo ${mut.attributeName} cambió en`, mut.target);
}
}
});
mutObserver.observe(document.body, {
childList: true, // detectar nodos añadidos/eliminados
subtree: true, // observar todo el subárbol
attributes: true, // detectar cambios de atributos
attributeFilter: ['class', 'data-state'], // solo estos atributos
});
Limpiar observadores correctamente
Todos los observers tienen un método disconnect() para dejar de observar todos los elementos de golpe, y unobserve(el) (solo IntersectionObserver y ResizeObserver) para elementos individuales. Desconéctate siempre que el componente se desmonte para evitar fugas de memoria:
// En React (useEffect):
useEffect(() => {
const observer = new ResizeObserver(callback);
observer.observe(ref.current);
return () => observer.disconnect(); // limpieza automática al desmontar
}, []);
