Observador de Intersecciones en JavaScript

Estefany Aguilar
12 min readSep 28, 2020

--

Hola ! Antes de comenzar, te quiero contar que este tema lo di en una charla en la comunidad de LimaJS en mayo del 2019. Aquí te comparto el enlace por si deseas ver la charla (empieza a partir del minuto 52:30):

https://www.youtube.com/watch?v=iC98gHBvAOg&t=3232s&ab_channel=LimaJS

Y ahora sí… comencemos !

Hoy en día, priorizar el contenido de la mitad superior de la página se volvió muy importante para el desarrollo de nuestros proyectos web, siendo la velocidad y el peso de la página uno de los aspectos más importantes.

Anteriormente, jugábamos con el scroll del navegador para que ciertos elementos del DOM aparecieran en determinados momentos, sin embargo, este procedimiento es algo costoso en términos de rendimiento. Es así como el Observador de Intersecciones viene a simplificar este trabajo y hacerlo menos costoso, ya que, la información proporcionada por esta API, permite que un desarrollador entienda fácilmente cuándo un elemento entra o sale de la vista, es decir que se puede usar para comprender el movimiento de los elementos DOM en relación con otro elemento o inclusive, con la ventana del navegador.

En este artículo, analizaremos algunas demostraciones y la relevancia del Observador de Interacciones en el futuro del desarrollo web.

💛 ¿Qué nos permite hacer el Observador de Intersecciones?

El Observador de Intersecciones puede ser útil para desarrollar de forma nativa características como:

  • Lazy Loading
  • Infinite Scroll
  • Highlight Current Section
  • Entre otros

Y veremos cómo se desarrollan los tres primeros en este artículo.

💛 Veamos un ejemplo para saber de qué se trata

En el ejemplo anterior podemos observar un elemento llamado “target” que al desplazarlo hacia arriba o hacia abajo con las flechas del teclado, sabremos si está dentro o fuera del elemento “root” (de margen gris). Cuando el elemento “target” ingresa al elemento “root” se le denomina “intersección”. El código lo veremos en detalle más adelante.

🔸 Hablemos de intersección y observación

Tradicionalmente los cálculos de posición de la web se basan en consultas explícitas del estado del DOM. Estas consultas pueden causar un recálculo en el diseño y puede ser redundante gracias al requisito de que los scripts encuesten para obtener esta información. Dada la oportunidad de reducir estos costos, llega esta API para simplificar la respuesta a estas consultas.

Esta API crea un IntersectionObserver con un elemento raíz; luego observa uno o más elementos que son descendientes de la raíz y activa el callback siempre que el elemento o los elementos observados crucen cualquiera de los umbrales del observador.

La raíz (root) puede ser la página completa o cualquier elemento del DOM (como en el ejemplo anterior) y por defecto, la observación comienza una vez que el objetivo ingresa a la raíz.

Veamos una imagen para que esto nos quede mucho más claro:

💛 ¿Qué necesitamos saber para usar el Observador de Intersecciones?

Lo más importante que necesitamos saber, es cómo funciona un observador genérico en la web moderna.

🔸 Observador Vs. Evento

La diferencia crucial entre el Evento regular y el Observador es que, de forma predeterminada, el primero reacciona de forma sincrónica cada vez que ocurre el Evento, lo que afecta la capacidad de respuesta del subproceso principal, mientras que el último debe reaccionar de forma asíncrona sin afectar tanto al rendimiento. Al menos, esto es cierto para los observadores que se presentan actualmente: todos ellos se comportan de forma asíncrona.

💛 Elementos principales para la configuración del Observador de Intersecciones

🔸 Constructor

El constructor crea un nuevo objeto y recibe un callback que es llamado cuando se detecta una intersección.

🔸 Propiedades

Este tipo de observador requiere una configuración con tres elementos principales: El root, el rootMargin y el threshold. Veamos cada uno en detalle a continuación.

  • El root

El root por defecto es la ventana gráfica de nuestro navegador, pero puede ser cualquier elemento del DOM. Los elementos que quieras observar deben vivir en root.

  • El rootMargin

El rootMargin es, como su nombre lo indica, una margen alrededor del root y tiene valores semejantes que en CSS: 10px 10px 10px 10px (top, right, bottom, left — manecillas del reloj). Esta propiedad expande o contrae el ‘marco de captura’ definido por root.

  • El threshold

No siempre se desea reaccionar instantáneamente cuando un elemento observado se cruza con un borde del “marco de captura” (definido como una combinación de root y rootMargin). El threshold define el porcentaje de dicha intersección a la que el Observador debe reaccionar.

🔸 Métodos

  • disconnect()

Evita que el objeto observe cualquier elemento. No tiene parámetros. Su sintaxis es: intersectionObserver.disconnect();

  • observe()

Agrega un elemento al conjunto de elementos de destino que está siendo observado por IntersectionObserver. Recibe como parámetro el elemento a observar que debe ser descendiente del elemento raíz. Su sintaxis es: IntersectionObserver.observe( targetElement );

  • takeRecords()

Devuelve una matriz de objetos para todos los objetivos observados que han experimentado un cambio de intersección desde la última vez que se verificaron, ya sea explícitamente mediante una llamada a este método o implícitamente mediante una llamada automática al callback del observador. No tiene parámetros. Su sintaxis es: intersectionObserverEntries = intersectionObserver.takeRecords();

  • unobserve()

Hace que el Objeto deje de observar un elemento en particular. Recibe como parámetro el elemento a observado. Su sintaxis es: IntersectionObserver.unobserve( target );

💛 ¿Qué debo hacer para crear un observador?

Lo primero que debemos hacer, es tener un elemento root (observador) y un elemento target (observado) como en el siguiente código de HTML:

<div id="root">
<div id="target">TARGET</div>
</div>

Algo que debemos tener presente, es que el elemento target debe ser un descendiente del elemento root para poder ser observado.

Cada elemento tiene un id para poder ser usado en JavaScript así:

const root = document.getElementById("root");
const target = document.getElementById("target");

Teniendo esto, veremos tres pasos a seguir para crear nuestro observador.

🔸 Pasos para crear un observador

  1. Escribimos un objeto con la configuración deseada de los tres elementos principales explicados anteriormente: root, rootMargin y threshold. Si este paso no se realiza, el observador tomará los valores por defecto.
const root = document.getElementById("root");const config = {
root: root,
rootMargin: '0px',
threshold: '0',
}

2. Pasamos ese objeto al constructor del Observador junto con el callback. Para esto, creamos una variable donde vamos a contener la referencia de este objeto. Este callback será ejecutado cada vez que el elemento target entre o salga del umbral del elemento root.

let io = new IntersectionObserver(function(entries){
...
}, config);

3. Como ya tenemos la instancia de nuestro objeto, ejecutamos el método observe() y le pasamos el elemento real a observar (target). Cuando el target cruce el umbral del elemento root, se ejecutará el callback.

const target = document.getElementById("target");io.observe(target);

🔸 Retomando el primer ejemplo

Veamos nuevamente nuestro primer ejemplo. Nuestro HTML esencial es:

<div id="root">
<div id="target">TARGET</div>
</div>

Para crear la lógica de nuestro callback, debemos pensar qué es lo que queremos hacer cuando el elemento “target” esté o no esté en el umbral del observador (llamado “root” en nuestro ejemplo).

En este caso en particular, queremos que exista un mensaje que cambie de color dependiendo si el elemento está dentro o fuera del umbral del observador: color rojo si está por fuera y color verde si está dentro. Por tal razón, debemos añadir a nuestro HTML dos nuevos elementos: status y statusContainer. El HTML nos quedaría de la siguiente forma:

<div id="statusContainer">
<h2 id="status">unknown</h2>
</div>
<div id="root" class="center">
<div id="target">TARGET</div>
</div>

La forma para acceder a los elementos del DOM lo haremos por medio de los selectores. Específicamente usaremos el selector por id y como argumento le pasaremos el id que previamente le agregamos a nuestros elementos HTML:

const statusContainer = document.getElementById("statusContainer");
const status = document.getElementById("status");
const root = document.getElementById("root");
const target = document.getElementById("target");

Como queremos que el texto del elemento cambie al intersectar con el observador, debemos hacer uso de una propiedad llamada isIntersecting de la interfaz IntersectionObserverEntry (la explicaremos con más detalle más adelante) para saber cuándo los dos elementos se están “tocando”. Entonces, tendremos el siguiente código:

status.textContent = entry.isIntersecting ? "Está intersecando 🥳" : "No está intersecando 😔";

Recordemos que los elementos del DOM tienen unas propiedades y gracias a ellas a través de JavaScript podemos cambiar estilos, agregar texto, etc. En este caso usamos la propiedad textContent que establece o devuelve el contenido del texto del nodo especificado (en este caso, el nodo especificado es “status”). También estamos usando un Operador Ternario de JavaScript y se llama así precisamente porque es el único operador que toma tres operandos: la condición, la expresión verdadera y la expresión falsa. Para este caso, la condición es saber si los elementos “root” y “target” se están intersecando o no. En caso de que se estén intersecando, el mensaje que se retorna es “Está intersecando” y si no, el mensaje es “No está intersecando”.

Pero, ¿Quién es entry? Te estarás preguntando. Vamos a escribir un console.log en el callback de nuestra instancia:

const io = new IntersectionObserver((entries) => {
console.log('ENTRIES: ', entries);
}, config);

Y esto es lo que nos muestra el console.log:

Las entries que estamos recibiendo en nuestro callback como array son de tipo IntersectionObserverEntry, una interfaz que nos proporciona un conjunto de propiedades predefinidas y precalculadas para cada elemento observado en particular.

Esta información dada por la interfaz viene de tres rectángulos diferentes, que definen las coordenadas y los límites de los elementos que están involucrados en este proceso de

Donde:

  • boundingClientRect: Un rectángulo para el propio elemento observado.
  • intersectionRatio: Indica qué parte del elemento target está actualmente visible dentro de la relación de intersección de la raíz, como un valor entre 0.0 y 1.0.
  • intersectionRect: Un área del “marco de captura” intersecado por el elemento observado.
  • isIntersecting: Es un dato booleano y nos dice si el elemento “target” ha intersecado con “root” (true) o no (false).
  • rootBounds: Un rectángulo para el “marco de captura” (root + rootMargin).
  • target: Es el elemento del DOM que es observado.
  • time: Indica el momento en que el elemento target experimenta el cambio de intersección El tiempo se especifica en milisegundos desde la creación del documento que lo contiene.

Por lo tanto, para este ejemplo tenemos que:

  • entries: es un array con una sola posición 0. Los valores de este array varían si el elemento entra o sale del umbral del observador.

Esto no quiere decir que siempre sea así. Para nuestro ejemplo si es así, pero la longitud del array entries depende de la cantidad de elementos target que sean observados por el root.

  • entry: es la posición 0 del array entries.

El dato que más nos interesa para nuestro ejemplo es “isIntersecting” (como bien pudimos ver anteriormente en el operador ternario) y debemos tener presente que el callback se ejecuta cada que el elemento sale o entra del umbral del observador.

Como tenemos un array, lo que hacemos es trabajar con él a través del método .map() así:

const io = new IntersectionObserver((entries) => {
entries.map(entry => {
status.textContent = entry.isIntersecting ? "Está intersecando 🥳" : "No está intersecando 😔";
});
}, config);

Esto con dibujitos sería:

Lo único que nos haría falta es cambiar el color del mensaje y lo haremos con un condicional:

if (entry.isIntersecting) {
statusContainer.className = "isIntersectingTrue";
} else {
statusContainer.className = "isIntersectingFalse";
}

Donde statusContainer es el div que contiene el texto y a quien le queremos cambiar el color de fondo, entonces le decimos que cuando se estén intersecando “target” con “root” se le agregue a statusContainer la clase de css “isIntersectingTrue” y si no se están intersecando, le agregue la clase “isIntersectingFalse”.

💛 Hablemos de rendimiento

Hoy en día trabajamos para mejorar el rendimiento y el tamaño de la carga útil que le brindamos a nuestros usuarios.

Muchas veces cuando trabajamos con imágenes de muy alta calidad, terminamos entregándole al usuario tamaños de archivos muy grandes que resultan ser costosos (en términos de rendimiento) para nuestra página o aplicación. Sin embargo, al usuario no le interesa que estén cargadas todas las imágenes (incluyendo aquellas que ni alcanza a ver), más bien le gustaría que la página cargue rápido.

Lazy Loading nos permite retrasar la carga o la inicialización de un objeto hasta el momento en el que sea utilizado. Esto permite que las aplicaciones que realicemos sean más eficientes, evitando así la precarga de objetos que podrían no llegar a utilizarse.

A continuación, veremos dos ejemplos de Lazy Loading: el primero, está hecho usando el evento scroll del navegador y el segundo, usando Intersection Observer API.

🔸 Lazy Loading usando el evento scroll

Cuando usamos la etiqueta de HTML <img/> la acompañamos con el atributo src para cargar las imágenes, sin embargo, el navegador al ver este atributo comenzará a descargar las imágenes de inmediato y eso no es lo que nosotros queremos para este caso. Por tanto, podemos confiar en algún atributo que comience con data como el atributo data-lazy o data-src.

<!-- Regular Image -->
<img src="img/Aurora.jpg">
<!-- Lazy Image -->
<img data-lazy="img/M81.jpg">

El siguiente ejemplo hace que las imágenes carguen cada que hacemos scroll, sin embargo presenta algunos inconvenientes:

  • No se escala bien en una página con muchas imágenes. Esto quiere decir que con pocas puede funcionar muy bien pero cuando tenemos muchas empiezan a existir problemas de memoria y rendimiento.
  • Al hacer scroll se disparan muchos eventos.
  • Cada que se dispara un evento, el navegador tiene que volver a calcular cada elemento en el DOM, lo cual resulta bastante ineficiente.

Utilizar este ejemplo en la vida real puede resultar siendo una tarea compleja, ya que tendríamos que buscar alternativas y estrategias para mejorar el rendimiento de la página.

🔸 Lazy Loading usando Intersection Observer API

En este ejemplo veremos que, a diferencia del anterior:

  • Escala muy bien al tener muchas imágenes.
  • No se disparan muchos eventos en cada cambio del desplazamiento en la pantalla.
  • No se necesita hacer cálculos en el DOM porque haremos uso de isIntersecting que nos dice si la imagen es visible o no y una vez esté visible, se puede desconectar el observador para optimizar aún más la eficiencia.

💛 Aplicaciones

Dos aplicaciones comunes al usar Intersection Observer son: Infinite Scroll y Highlight Current Section.

🔸 Infinite Scroll

Este caso de uso es bastante interesante ya que se está implementado con la librería de React JS. Esto quiere decir que podemos usar Intersection Observer en nuestros proyectos que usan React JS, Angular, Vue, etc.

o que hicimos fue básicamente configurar el observador en el método del ciclo de vida componentDidMount(), ya que queremos que inicie una vez el componente esté montado.

Adicionalmente, nuestro callback llamado handleObserver hace lo siguiente:

  1. En el condicional se asegura de que solo se llame al cuerpo del método cuando se desplaza hacia abajo de la pantalla y no cuando se desplaza hacia arriba.
  2. La paginación en Github usa los ID de los elementos en lugar del número de página. En este caso necesitamos la última identificación de la lista actual para tomar decisiones para la próxima. Las variablea lastUser y curePage ayudan a mantener esto.
  3. Cuando curePage haya actualizado, podemos llamar getUsers con su valor y actualizar el estado con setState.

🔸 Highlight Current Section

En el siguiente ejemplo veremos nuevamente el reemplazo del evento scroll por el uso de Intersection Observer. Para este caso tendremos un navbar fijo en donde debemos colorear la sección actual dependiendo de la posición de desplazamiento de la página.

Una de las cosas interesantes en este ejemplo es la configuración:

const config = {
rootMargin: '-50px 0px -55%',
threshold: 0.8,
};

Aquí el rootMargin ya no es 0px puesto que en los ejemplos anteriores (como Lazy Loading) lo que queríamos era que cargara antes de que entrara a la vista, pero aquí, queremos que se resalte la sección cuando esta esté visible, no antes.

Por lo tanto, queremos que el observador detecte los elementos que entran en el rootMargin entre 50px desde la parte superior y el 55% de la vista desde la parte inferior, es decir que es como si se creara una pequeña ventana de observación.

💛 Compatibilidad en navegadores

Es importante tener en cuenta la compatibilidad entre Intersection Observer y el navegador. En esta tabla de Can I Use podemos ver qué navegadores son compatibles con esta API.

Todo lo que se muestra en color verde son las versiones compatibles con esta API. Sin embargo, en algunas versiones de Firefox, Chrome, Safari, etc., el color rojo nos indica que esta API aún no es compatible. Además, puedes mirar en la parte superior derecha que el uso global es del 76.93%.

La mayoría de los navegadores lo admiten y es lo suficientemente fuerte como para que puedas comenzar a implementarlo en tu sitio web.

Pero ¿qué pasa con los navegadores que no tienen soporte? Lo que puedes hacer es usar un polyfill en esos casos.

💛 Conclusión

El trabajo que realiza Intersection Observer es increíble y puede aportar mejoras en el rendimiento de nuestros desarrollos. También, el soporte con los navegadores está bien para comenzar a usarlo, aunque claro, aún hay mucho trabajo qué hacer al respecto.

💛 Herramientas usadas

💛 Referencias

Bai

--

--