Regresar

lectura estimada

Publicado en: 6 jun 2025

Programming

Cambiar idioma:

La API Intersection Observer

Una de las primeras cosas que me interesaron cuando comencé mi camino en el desarrollo web fue lograr el efecto de aparecer al hacer scroll. Honestamente, no sé por qué, pero realmente necesitaba agregarlo a cada sitio que creaba.

En ese entonces, no teníamos la API “IntersectionObserver”. En su lugar, teníamos que depender de algunas prácticas de codificación cuestionables usando la API “getBoundingClientRect”. Veamos:

function is_element_in_viewport(el: HTMLElement): boolean {
  const rect = el.getBoundingClientRect();
  return (
    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  );
}

const elements = document.querySelectorAll('.reveal');

function check_visibility() {
  let i = 0;
  while(i < elements.length) {
    if(is_element_in_viewport(elements[i])) {
      el.classList.add('visible')
    }
    i++;
  }
}

window.addEventListener('scroll', checkVisibility);
window.addEventListener('resize', checkVisibility);

check_visibility();

Aunque es bastante comprensible, esto es malo por varias razones:

CaracterísticagetBoundingClientRect() + ScrollIntersectionObserver
Rendimiento de CPU❌ Puede activarse en cada evento de scroll, incluso para docenas o cientos de elementos✅ Altamente optimizado internamente
Consumo de batería en móviles❌ Mayor debido a comprobaciones constantes✅ Menor, usa eventos asíncronos
Personalización⚠️ Más difícil de ajustar (por ejemplo, visibilidad parcial)✅ Opciones simples de “threshold” y “rootMargin”
Limpieza del código❌ Desordenado con listeners y limpieza✅ Lógica encapsulada
Auto unobserve cuando es necesario❌ Requiere seguimiento y limpieza manual✅ Incorporado con “.unobserve()“

¿Qué es IntersectionObserver?

La API IntersectionObserver permite a los desarrolladores observar de forma asíncrona los cambios en la intersección de un elemento objetivo con un elemento ancestro o el viewport.

En términos más simples, te dice cuándo un elemento entra o sale del área visible de la pantalla (u otro contenedor).

¿Por qué usarla?

Antes de “IntersectionObserver”, dependíamos de eventos de scroll combinados con cálculos manuales para determinar la posición de un elemento. Esto no solo era engorroso, sino también muy ineficiente en cuanto a rendimiento. Y no voy a mentir, algunos de mis proyectos antiguos todavía usan esos métodos, no es bonito.

Casos de uso comunes para IntersectionObserver:

  • Carga diferida (lazy-loading) de imágenes o componentes
  • Disparar animaciones cuando los elementos entran en vista
  • Listas de scroll infinito
  • Reportar impresiones de anuncios
  • Observar visibilidad para analíticas o accesibilidad

Haciendo que “IntersectionObserver” sea eficiente

Para aprovechar al máximo esta API, sigamos las mejores prácticas:

Usa una sola instancia de observer cuando sea posible

Crear un solo “IntersectionObserver” y reutilizarlo para varios elementos es más eficiente que instanciar muchos observers.

Vi este código:


for (let i = 0; i < elements.length; i++) {
  const el = elements[i];
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
      }
    });
  }, { threshold: 0.1 });
  observer.observe(el);
}

Esto, por supuesto, es una mala noticia: el desarrollador está creando una nueva instancia de observer para cada elemento que quiere observar, ¡eso es demasiado!

Usa “rootMargin” y “threshold” sabiamente

  • “rootMargin” puede usarse para disparar la intersección antes de que un elemento sea completamente visible.
  • “threshold” ayuda a ajustar la sensibilidad para la visibilidad parcial.

Desconecta cuando termines

Llama a “observer.unobserve()” o “observer.disconnect()” cuando los elementos ya no necesiten ser observados (por ejemplo, una vez que se han cargado o animado).


Un Observer limpio y reutilizable con Closures

Aquí tienes un ejemplo de una función basada en closures que uso en mis proyectos para simplificar la configuración de observers:

export type List = NodeListOf<HTMLElement> | Array<HTMLElement>

export default function (
    call_back: (entry: IntersectionObserverEntry) => void,
    options?: IntersectionObserverInit
) {
    const observer = new IntersectionObserver((entries) => {
        let i = 0;
        while (i < entries.length) {
            call_back(entries[i])
            i++;
        }
    }, options);

    return (nodes: List, optional_call_back?: (node: HTMLElement) => void) => {
        let j = 0;
        while (j < nodes.length) {
            if (optional_call_back) optional_call_back(nodes[j]);
            observer.observe(nodes[j]);
            j++;
        }
    };
}

Beneficios de este patrón:

  1. Basado en closures: El observer interno está encapsulado de forma privada, haciendo que la API sea limpia y libre de efectos secundarios.
  2. Reutilizable: Defines la lógica del callback una sola vez y la aplicas a muchos conjuntos de elementos.
  3. Personalizable: Callback opcional por nodo para configuración adicional (por ejemplo, agregar una clase o establecer un atributo).
  4. DRY: Elimina el código repetitivo al configurar múltiples observers en una aplicación! (Don’t repeat yourself).
  5. ¡Solo un loop para aplicar estilos y observadores!

Ejemplo de uso:

const observe = createObserver((entry) => {
    if (entry.isIntersecting) {
        entry.target.classList.add('visible');
    }
}, {threshold: 0.1});

const nodes = document.querySelectorAll('.reveal') as NodeListOf<HTMLElement>;
observe(nodes, (node) => node.classList.add('pre-reveal'));

Conclusión

La API “IntersectionObserver” es una herramienta poderosa y eficiente en el conjunto moderno de herramientas de front-end. Al usar closures y abstracciones como el ejemplo anterior, puedes mantener tu base de código limpia, eficiente y reutilizable.

Quizás te interese: