Intersection Observer: Casos de uso

Intersection Observer: Casos de uso

¿Alguna vez te has encontrado en la situación donde necesitas ejecutar una acción SOLO cuando cierto elemento es visible para el usuario? El equipo detrás de Javascript sabe que históricamente esto ha sido una tarea difícil por lo que nos trae el API Intersection Observer.

¿Qué podemos hacer con esta API?

  • Implementar lazy loading: cargar imágenes y recursos sólo cuando el usuario está a punto de ocuparlos, mejorando así el performance de nuestra web.
  • Mostrar una barra de progreso de lectura en una publicación.
  • Reproducir/pausar un video dependiendo de si es visible para el usuario.
  • Contar las veces que un usuario vio un elemento (útil para banners publicitarios).
  • Scrolling infinito.
  • Animaciones tipo scroll animations.

Estoy segura de que ya tendrás alguno que otro uso en mente, sería genial que los dejaras en los comentarios. Ahora, antes de mostrarte cómo implementarlo, debemos tener algunos conceptos claros.

🖥 Viewport

Es el área (normalmente rectangular) visible para el usuario. El tamaño del mismo depende del dispositivo. Valga la redundancia y para que quede claro: todo lo que esté fuera del viewport, no es visible para el usuario.

Podemos notar la porción de la página que se encuentra en el viewport y la que no es visible al usuario

⩕Intersección

En este contexto, una intersección es el encuentro entre dos elementos que se cortan mutuamente. Tomando la imagen anterior como ejemplo, uno de los elementos sería el contenido de la página web, otro elemento sería el viewport; y la intersección (el solapamiento) de ellos es la porción de contenido que está viendo el usuario. Esto también se puede dar con elementos HTML padres y sus hijos. Si seguimos el siguiente codepen, la intersección entre `.padre` e `.hijo` sería el texto que podemos visualizar.

✨ Intersection Observer

Según la MDN Web Docs:

La API Intersection Observer provee una vía asíncrona para observar cambios en la intersección de un elemento con un elemento ancestro o con el viewport del documento de nivel superior.

Implementación

Lo primero que debemos hacer es instanciar el observer:

const observer = new IntersectionObserver(callback, options);

Miremos en detalle los parámetros que recibe el constructor.

Callback

Esta función recibe una lista de objetos de tipo IntersectionObserverEntry y el observer.

const callback = (entries, observer) => {
    // Por cada entrada (elemento observado)
	entries.forEach((entry) => {
        if (entry.isIntersecting) {
          // El elemento es visible
          // Acá puedo ejecutar lo que desee.

          // Pro tip: Si no vas a necesitar observar más cambios, puedes remover el observer.
          observer.unobserve(entry.target);
        }
  });
}

/**
	Estructura de un entry:
    {
    	boundingClientRect
        intersectionRatio
        intersectionRect
        isIntersecting
        rootBounds
        target
        time
    }
*/

⚠️ Importante: El callback debe ejecutarse rápidamente para no bloquear el event loop.

Options

root

Es el elemento principal (el ancestro). Por defecto es el viewport del navegador, pero cualquier elemento del documento puede serlo.

rootMargin

Es el margen alrededor del root que será parte del área a evaluar. Por ejemplo, si tengo un elemento cuya altura es de 100px, puedo agregarle un `rootMargin` de 10px. Esos pixeles extras, serán tomados en cuenta al momento de evaluar si se está dando la intersección o no.

threshold

En lugar de reportar cada mínimo cambio indicando cómo de visible es el elemento que observamos, la Intersection Observer API usa umbrales.

El threshold (umbral) es un flotante (o array de ellos) que indica a qué porcentaje de visibilidad del elemento observado se va a ejecutar el callback. Éste va de 0.0 a 1.0, donde 0.0 significa que no es visible, 0.5 que se ve la mitad, y 1.0 que es completamente visible. Por defecto su valor es 0, y significa que se ejecutará el callback en cuanto el elemento empiece a intersectar.

En el siguiente codepen, tomado de MDN Web Docs, podrás ver cómo se comportan estos umbrales:

Al final nuestro `options` quedaría de la siguiente manera:

const options = {
	root: null, // El viewport será el valor.
    rootMargin: 10px,
	threshold: [0.25, 0.5, 0.5, 1.0] // El callback se ejecutará cada vez que el elemento sea 25 % más/menos visible.
}

Casos de Uso

Ahora que sabemos cómo podemos implementarlo, veamos dos ejemplos sencillos que se pueden extender.

🟣 Lazy Loading en Imágenes

Asumamos que tenemos una lista de imágenes. No queremos que todas carguen al mismo tiempo, porque eso no ayuda al performance de nuestra web. Lo deseable es que carguen sólo cuando entren en el viewport del dispositivo del usuario.

Para lograr nuestro objetivo, vamos a utilizar un concepto que ya vimos anteriormente en una plática: data attributes (si no la has visto, corre a verla aquí).

En nuestros elementos `img`, vamos a establecer como fuente inicial una imagen placeholder, que cargará una sola vez y ya luego se repetirá para los elementos que aún no son visibles. La verdadera fuente de la imagen estará en un atributo custom llamado `data-src`.

<img 
     src='placeholder.png'
     data-src='mi-imagen.png'
     alt='mi imagen'
/>

Todo cool hasta ahora. Pero tenemos que buscar la manera de que cuando la imagen esté por entrar al viewport, esa fuente cambie a la que realmente queremos mostrar. Para esto usaremos un Intersection Observer.

const callback = (entries, observer) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      // Tomamos el valor de data-src y lo seteamos en
      // el atributo src
      entry.target.src = entry.target.dataset.src;

      // Como la imagen cargará una vez, dejamos de observar el elemento.
      observer.unobserve(entry.target);
    }
  });
};

const options = {
  root: null, // Será el viewport. Esta línea la podemos omitir.
  rootMargin: '0px 0px -100px 0px', // Cuando la imagen esté a 100px de aparecer, ejecuto el callback.

  // Nota que no estoy seteando el threshold
  // El callback se ejecutará en cuanto el elemento empiece a ser visible (incluyendo el margen).
};

const observer = new IntersectionObserver(callback, options);
document.querySelectorAll('img').forEach((img) => {
  observer.observe(img);
});

Veámoslo en acción:

Qué trucazo, no? (gif)

Te invito a que juegues con los umbrales y veas cómo cambia el comportamiento.

🟣 Notificación al ver todo el contenido

Digamos que nos esforzamos por escribir una buena publicación y queremos que el usuario vea una notificación donde le agradecemos por llegar hasta el final. Veamos cómo podemos hacerlo:

Esta vez te invito a analizar el código del pen anterior.


¿Habías usado esta API antes?

Si tienes cualquier duda la puedes dejar en los comentarios o escribirme por twitter: @musartedev.

¡Gracias por leer y espero te sea de mucha utilidad!

Referencias / Aprende más