Observers en JavaScript: IntersectionObserver, ResizeObserver y MutationObserver

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
}, []);

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP