Now You See Me: cómo diferir, cargar de forma diferida y actuar con IntersectionObserver
Publicado: 2022-03-10Érase una vez un desarrollador web que convenció con éxito a sus clientes de que los sitios no deberían tener el mismo aspecto en todos los navegadores, se preocupaba por la accesibilidad y fue uno de los primeros en adoptar las cuadrículas CSS. Pero en el fondo de su corazón, la interpretación era su verdadera pasión: constantemente optimizaba, minimizaba, supervisaba e incluso empleaba trucos psicológicos en sus proyectos.
Entonces, un día, aprendió sobre imágenes de carga diferida y otros activos que no son visibles de inmediato para los usuarios y que no son esenciales para mostrar contenido significativo en la pantalla. Era el comienzo del amanecer: el desarrollador entró en el mundo malvado de los complementos de jQuery de carga diferida (o tal vez en el mundo no tan malvado de los atributos async
y defer
). Algunos incluso dicen que llegó directamente al núcleo de todos los males: el mundo de los oyentes de eventos de scroll
. Nunca sabremos con certeza dónde terminó, pero, de nuevo, este desarrollador es absolutamente ficticio, y cualquier similitud con cualquier desarrollador es mera coincidencia.
Bueno, ahora puedes decir que se ha abierto la caja de Pandora y que nuestro desarrollador ficticio no hace que el problema sea menos real. Hoy en día, priorizar el contenido de la mitad superior de la página se volvió absolutamente importante para el rendimiento de nuestros proyectos web, tanto desde el punto de vista de la velocidad como del peso de la página.
En este artículo, vamos a salir de la oscuridad del scroll
y hablar sobre la forma moderna de cargar recursos de forma diferida. No solo imágenes de carga diferida, sino cargando cualquier activo para el caso. Más aún, la técnica de la que hablaremos hoy es capaz de mucho más que activos de carga diferida: podremos proporcionar cualquier tipo de funcionalidad diferida basada en la visibilidad de los elementos para los usuarios.
Damas y caballeros, hablemos de la API Intersection Observer. Pero antes de comenzar, echemos un vistazo al panorama de las herramientas modernas que nos llevó a IntersectionObserver
.
2017 fue un muy buen año para las herramientas integradas en nuestros navegadores, que nos ayudaron a mejorar la calidad y el estilo de nuestro código base sin demasiado esfuerzo. En estos días, la web parece estar alejándose de soluciones esporádicas basadas en soluciones muy diferentes a muy típicas a un enfoque más bien definido de las interfaces de Observer (o simplemente "Observadores"): MutationObserver, que cuenta con un buen soporte, obtuvo nuevos miembros de la familia que rápidamente adoptado en los navegadores modernos:
- IntersecciónObservador y
- PerformanceObserver (como parte de la especificación Performance Timeline Level 2).
Otro miembro potencial de la familia, FetchObserver, es un trabajo en progreso y nos guía más hacia las tierras de un proxy de red, pero hoy me gustaría hablar más sobre el front-end.
PerformanceObserver
e IntersectionObserver
tienen como objetivo ayudar a los desarrolladores front-end a mejorar el rendimiento de sus proyectos en diferentes puntos. El primero nos brinda la herramienta para el Monitoreo de Usuario Real, mientras que el segundo es la herramienta que nos brinda una mejora tangible del rendimiento. Como se mencionó anteriormente, este artículo analizará en detalle exactamente el último: IntersectionObserver . Para comprender la mecánica de IntersectionObserver
en particular, deberíamos echar un vistazo a cómo se supone que funciona un Observer genérico en la web moderna.
Sugerencia profesional : puede omitir la teoría y sumergirse en la mecánica de IntersectionObserver de inmediato o, incluso más, directamente a las posibles aplicaciones de IntersectionObserver
.
Observador vs Evento
Un "Observador", como su nombre lo indica, está destinado a observar algo que sucede en el contexto de una página. Los observadores pueden ver algo que sucede en una página, como cambios en el DOM. También pueden observar los eventos del ciclo de vida de la página. Los observadores también pueden ejecutar algunas funciones de devolución de llamada. Ahora, el lector atento podría detectar inmediatamente el problema aquí y preguntar: “Entonces, ¿cuál es el punto? ¿No tenemos eventos para este propósito ya? ¿Qué hace a los Observadores diferentes?” ¡Muy buen punto! Echemos un vistazo más de cerca y resolvámoslo.
La diferencia crucial entre el evento normal y el observador es que, de forma predeterminada, el primero reacciona de forma sincrónica cada vez que se produce el evento, lo que afecta la capacidad de respuesta del subproceso principal, mientras que el segundo debería reaccionar de forma asíncrona sin afectar tanto al rendimiento. Al menos, esto es cierto para los Observers presentados actualmente: todos se comportan de forma asíncrona y no creo que esto cambie en el futuro.
Esto lleva a la principal diferencia en el manejo de las devoluciones de llamada de los observadores que podría confundir a los principiantes: la naturaleza asíncrona de los observadores puede dar como resultado que varios observables pasen a una función de devolución de llamada al mismo tiempo. Debido a esto, la función de devolución de llamada no debe esperar una sola entrada, sino una Array
de entradas (aunque a veces la matriz contendrá solo una entrada).
Además, algunos observadores (en particular, el que estamos hablando hoy) brindan propiedades precalculadas muy útiles que, de lo contrario, usamos para calcularnos usando métodos y propiedades costosos (desde el punto de vista del rendimiento) cuando usamos eventos regulares. Para aclarar este punto, veremos un ejemplo un poco más adelante en el artículo.
Entonces, si es difícil para alguien apartarse del paradigma de eventos, diría que los observadores son eventos con esteroides. Otra descripción sería: Los observadores son un nuevo nivel de aproximación sobre los eventos. Pero independientemente de la definición que prefiera, no hace falta decir que los observadores no están destinados a reemplazar eventos (al menos no todavía); hay suficientes casos de uso para ambos, y pueden vivir felizmente uno al lado del otro.
Estructura del observador genérico
La estructura genérica de un observador (cualquiera de los disponibles en el momento de escribir este artículo) se parece a esto:
/** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);
Nuevamente, tenga en cuenta que entries
son una Array
de valores, no una sola entrada.
Esta es la estructura genérica: las implementaciones de Observers particulares difieren en los argumentos que se pasan a su observe()
y los argumentos que se pasan a su devolución de llamada. Por ejemplo, MutationObserver
también debería obtener un objeto de configuración para saber más sobre qué cambios observar en el DOM. PerformanceObserver
no observa nodos en DOM, sino que tiene el conjunto dedicado de tipos de entrada que puede observar.
Aquí, terminemos la parte "genérica" de esta discusión y profundicemos en el tema del artículo de hoy: IntersectionObserver
.
Deconstruyendo IntersectionObserver
En primer lugar, averigüemos qué es IntersectionObserver
.
Según MDN:
La API Intersection Observer proporciona una forma de observar de forma asíncrona los cambios en la intersección de un elemento de destino con un elemento antepasado o con la ventana gráfica de un documento de nivel superior.
En pocas palabras, IntersectionObserver
observa de forma asíncrona la superposición de un elemento por otro elemento. Hablemos de para qué sirven esos elementos en IntersectionObserver
.
Inicialización de IntersectionObserver
En uno de los párrafos anteriores, hemos visto la estructura de un Observador genérico. IntersectionObserver
amplía un poco esta estructura. En primer lugar, este tipo de Observer requiere una configuración con tres elementos principales:
-
root
: Este es el elemento raíz utilizado para la observación. Define el "marco de captura" básico para los elementos observables. De forma predeterminada, laroot
es la ventana gráfica de su navegador, pero en realidad puede ser cualquier elemento en su DOM (luego configuraroot
en algo comodocument.getElementById('your-element')
). Sin embargo, tenga en cuenta que los elementos que desea observar deben "vivir" en el árbol DOM deroot
en este caso.
-
rootMargin
: define el margen alrededor de su elementoroot
que extiende o reduce el "marco de captura" cuando las dimensiones de suroot
no brindan suficiente flexibilidad. Las opciones para los valores de esta configuración son similares a las demargin
en CSS, comorootMargin: '50px 20px 10px 40px'
(arriba, abajo a la derecha, a la izquierda). Los valores se pueden abreviar (comorootMargin: '50px'
) y se pueden expresar enpx
o%
. Por defecto,rootMargin: '0px'
.
-
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 deroot
yrootMargin
). Elthreshold
define el porcentaje de tal intersección en el que el observador debe reaccionar. Se puede definir como un valor único o como una matriz de valores. Para comprender mejor el efecto delthreshold
(sé que a veces puede ser confuso), aquí hay algunos ejemplos:-
threshold: 0
: el valor predeterminadoIntersectionObserver
debe reaccionar cuando el primer o último píxel de un elemento observado cruza uno de los bordes del "marco de captura". Tenga en cuenta queIntersectionObserver
es independiente de la dirección, lo que significa que reaccionará en ambos escenarios: a) cuando el elemento ingresa yb) cuando sale del "marco de captura". -
threshold: 0.5
: el observador debe dispararse cuando el 50 % de un elemento observado se cruza con el "marco de captura"; -
threshold: [0, 0.2, 0.5, 1]
: El observador debe reaccionar en 4 casos:- El primer píxel de un elemento observado entra en el "marco de captura": el elemento todavía no está realmente dentro de ese marco, o el último píxel del elemento observado sale del "marco de captura": el elemento ya no está dentro del marco;
- El 20% del elemento está dentro del "marco de captura" (de nuevo, la dirección no importa para
IntersectionObserver
); - El 50% del elemento está dentro del “marco de captura”;
- El 100% del elemento está dentro del “marco de captura”. Esto es estrictamente opuesto al
threshold: 0
.
-
Para informar a nuestro IntersectionObserver
de nuestra configuración deseada, simplemente pasamos nuestro objeto de config
al constructor de nuestro Observer junto con nuestra función de devolución de llamada como esta:
const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);
Ahora, debemos darle a IntersectionObserver
el elemento real para observar. Esto se hace simplemente pasando el elemento a la función observe()
:
… const img = document.getElementById('image-to-observe'); observer.observe(image);
Un par de cosas a tener en cuenta sobre este elemento observado:
- Se ha mencionado anteriormente, pero vale la pena mencionarlo nuevamente: en caso de que establezca
root
como un elemento en el DOM, el elemento observado debe ubicarse dentro del árbol DOM deroot
. -
IntersectionObserver
solo puede aceptar un elemento para la observación a la vez y no admite el suministro por lotes para las observaciones. Esto significa que si necesita observar varios elementos (digamos varias imágenes en una página), debe iterar sobre todos ellos y observar cada uno de ellos por separado:
… const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
- Al cargar una página con Observer en su lugar, es posible que observe que la devolución de llamada de
IntersectionObserver
se ha activado para todos los elementos observados a la vez. Incluso aquellos que no coinciden con la configuración suministrada. “Bueno… no es realmente lo que esperaba”, es el pensamiento habitual cuando se experimenta esto por primera vez. Pero no se confunda aquí: esto no significa necesariamente que esos elementos observados de alguna manera se crucen con el "marco de captura" mientras se carga la página.
Sin embargo, lo que significa es que la entrada para este elemento se inicializó y ahora está controlada por su IntersectionObserver
. Sin embargo, esto podría agregar ruido innecesario a su función de devolución de llamada, y se convierte en su responsabilidad detectar qué elementos realmente se cruzan con el "marco de captura" y cuáles aún no necesitamos tener en cuenta. Para entender cómo hacer esa detección, profundicemos un poco más en la anatomía de nuestra función de devolución de llamada y veamos en qué consisten dichas entradas.
Devolución de llamada de IntersectionObserver
En primer lugar, la función de devolución de llamada para IntersectionObserver
toma dos argumentos, y hablaremos de ellos en orden inverso, comenzando con el segundo argumento. Junto con la Array
de entradas observadas antes mencionada, que se cruzan con nuestro "marco de captura", la función de devolución de llamada obtiene el propio Observador como segundo argumento.
Referencia al propio observador
new IntersectionObserver(function(entries, SELF) {…});
Obtener la referencia al observador en sí es útil en muchos escenarios cuando desea dejar de observar algún elemento después de que IntersectionObserver
lo haya detectado por primera vez. Los escenarios como la carga diferida de las imágenes, la recuperación diferida de otros activos, etc. son de este tipo. Cuando desea dejar de observar un elemento, IntersectionObserver
proporciona un método unobserve(element-to-stop-observing)
que se puede ejecutar en la función de devolución de llamada después de realizar algunas acciones en el elemento observado (como la carga diferida real de una imagen, por ejemplo ).
Algunos de estos escenarios se revisarán más adelante en el artículo, pero con este segundo argumento fuera de nuestro camino, pasemos a los actores principales de este juego de devolución de llamada.
IntersecciónObservadorEntrada
new IntersectionObserver(function(ENTRIES, self) {…});
Las entries
que obtenemos en nuestra función de devolución de llamada como una Array
son del tipo especial: IntersectionObserverEntry
. Esta interfaz nos proporciona un conjunto predefinido y precalculado de propiedades relativas a cada elemento observado en particular. Echemos un vistazo a los más interesantes.
En primer lugar, las entradas de tipo IntersectionObserverEntry
vienen con información sobre tres rectángulos diferentes, que definen las coordenadas y los límites de los elementos involucrados en el proceso:
-
rootBounds
: un rectángulo para el "marco de captura" (root
+rootMargin
); -
boundingClientRect
: un rectángulo para el propio elemento observado; -
intersectionRect
: Un área del "marco de captura" intersecado por el elemento observado.
Lo realmente genial de que estos rectángulos se calculen para nosotros de forma asincrónica es que nos brinda información importante relacionada con el posicionamiento del elemento sin que llamemos a getBoundingClientRect()
, offsetTop
, offsetLeft
y otras propiedades y métodos de posicionamiento costosos que desencadenan la paliza del diseño. Pura victoria para el rendimiento!
Otra propiedad de la interfaz IntersectionObserverEntry
que nos resulta interesante es isIntersecting
. Esta es una propiedad de conveniencia que indica si el elemento observado actualmente se cruza con el "marco de captura" o no. Podríamos, por supuesto, obtener esta información observando la intersectionRect
(si este rectángulo no es 0×0, el elemento se cruza con el "marco de captura"), pero tener esto precalculado para nosotros es bastante conveniente.
isIntersecting
se puede utilizar para averiguar si el elemento observado acaba de entrar en el "marco de captura" o si ya lo está saliendo. Para averiguarlo, guarde el valor de esta propiedad como una bandera global y cuando la nueva entrada para este elemento llegue a su función de devolución de llamada, compare su nuevo isIntersecting
con esa bandera global:
- Si era
false
y ahora estrue
, entonces el elemento está entrando en el "marco de captura"; - Si es lo contrario y es
false
ahora mientras que antes eratrue
, entonces el elemento está saliendo del "marco de captura".
isIntersecting
es exactamente la propiedad que nos ayuda a resolver el problema que discutimos anteriormente, es decir, entradas separadas para los elementos que realmente intersecan el "marco de captura" del ruido de aquellos que son solo la inicialización de la entrada.
let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);
NOTA : En Microsoft Edge 15, la propiedad isIntersecting
no se implementó, devolviéndose undefined
a pesar de la compatibilidad total con IntersectionObserver
en caso contrario. Sin embargo, esto se solucionó en julio de 2017 y está disponible desde Edge 16.
La interfaz IntersectionObserverEntry
proporciona una propiedad de conveniencia más precalculada: intersectionRatio
. Este parámetro se puede usar para los mismos propósitos que isIntersecting
pero proporciona un control más granular debido a que es un número de coma flotante en lugar de un valor booleano. El valor de la relación de intersectionRatio
indica la cantidad del área del elemento observado que se cruza con el "marco de captura" (la relación entre el área de intersectionRect
y el área de boundingClientRect
del Recto del cliente). Nuevamente, podríamos hacer este cálculo nosotros mismos usando la información de esos rectángulos, pero es bueno que lo hagan por nosotros.
target
es una propiedad más de la interfaz IntersectionObserverEntry
a la que puede necesitar acceder con bastante frecuencia. Pero no hay absolutamente nada de magia aquí, es solo el elemento original que se pasó a la función de observe()
de su Observador. Al igual que event.target
al que te has acostumbrado cuando trabajas con eventos.
Para obtener la lista completa de propiedades de la interfaz IntersectionObserverEntry
, verifique la especificación.
Aplicaciones posibles
Me doy cuenta de que probablemente llegaste a este artículo exactamente por este capítulo: ¿a quién le importa la mecánica cuando tenemos fragmentos de código para copiar y pegar después de todo? Así que no lo molestaremos con más discusión ahora: estamos entrando en la tierra del código y los ejemplos. Espero que los comentarios incluidos en el código aclaren las cosas.
Funcionalidad diferida
En primer lugar, revisemos un ejemplo que revela los principios básicos que subyacen a la idea de IntersectionObserver
. Digamos que tiene un elemento que tiene que hacer muchos cálculos una vez que está en la pantalla. Por ejemplo, su anuncio debería registrar una vista solo cuando se haya mostrado realmente a un usuario. Pero ahora, imaginemos que tiene un elemento de carrusel de reproducción automática en algún lugar debajo de la primera pantalla de su página.
Ejecutar un carrusel, en general, es una tarea pesada. Por lo general, implica temporizadores de JavaScript, cálculos para desplazarse automáticamente por los elementos, etc. Todas estas tareas cargan el hilo principal, y cuando se hace en modo de reproducción automática, es difícil para nosotros saber cuándo nuestro hilo principal recibe este golpe. Cuando estamos hablando de priorizar el contenido en nuestra primera pantalla y queremos presionar Primera pintura significativa y Tiempo para interactivo lo antes posible, el hilo principal bloqueado se convierte en un cuello de botella para nuestro rendimiento.
Para solucionar el problema, podríamos diferir la reproducción de dicho carrusel hasta que llegue a la ventana gráfica del navegador. Para este caso, emplearemos nuestro conocimiento y ejemplo para el parámetro isIntersecting
de la interfaz IntersectionObserverEntry
.
const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);
Aquí, jugamos el carrusel solo cuando entra en nuestra ventana gráfica. Observe la ausencia del objeto de config
pasado a la inicialización de IntersectionObserver
: esto significa que confiamos en las opciones de configuración predeterminadas. Cuando el carrusel sale de nuestra ventana gráfica, debemos dejar de reproducirlo para no gastar recursos en los elementos que ya no son importantes.
Carga diferida de activos
Este es, probablemente, el caso de uso más obvio para IntersectionObserver
: no queremos gastar recursos para descargar algo que el usuario no necesita en este momento. Esto brindará un gran beneficio a sus usuarios: los usuarios no necesitarán descargar y sus dispositivos móviles no necesitarán analizar y recopilar mucha información inútil que no necesitan en este momento. Como era de esperar, también ayudará al rendimiento de su aplicación.
Anteriormente, para diferir la descarga y el procesamiento de recursos hasta el momento en que el usuario pudiera verlos en la pantalla, lidiamos con detectores de eventos en eventos como scroll
. El problema es obvio: esto disparó a los oyentes con demasiada frecuencia. Así que tuvimos que pensar en la idea de acelerar o eliminar el rebote de la ejecución de la devolución de llamada. Pero todo esto añadió mucha presión a nuestro hilo principal y lo bloqueó cuando más lo necesitábamos.
Entonces, volviendo a IntersectionObserver
en un escenario de carga diferida, ¿qué debemos vigilar? Veamos un ejemplo simple de imágenes de carga diferida.
Intente desplazarse lentamente por esa página hasta la "tercera pantalla" y observe la ventana de seguimiento en la esquina superior derecha: le permitirá saber cuántas imágenes se han descargado hasta el momento.
En el núcleo del marcado HTML para esta tarea se encuentra una secuencia simple de imágenes:
… <img data-src="https://blah-blah.com/foo.jpg"> …
Como puede ver, las imágenes deben venir sin etiquetas src
: una vez que un navegador ve el atributo src
, comenzará a descargar esa imagen de inmediato que es opuesta a nuestras intenciones. Por lo tanto, no deberíamos poner ese atributo en nuestras imágenes en HTML y, en su lugar, podríamos confiar en algún atributo de data-
como data-src
aquí.
Otra parte de esta solución es, por supuesto, JavaScript. Centrémonos en las partes principales aquí:
const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });
En cuanto a la estructura, no hay nada nuevo aquí: hemos cubierto todo esto antes:
- Recibimos todos los mensajes con nuestros atributos
data-src
; - Establecer
config
: para este escenario, desea expandir su "marco de captura" para detectar elementos un poco más abajo que la parte inferior de la ventana gráfica; - Registre
IntersectionObserver
con esa configuración; - Iterar sobre nuestras imágenes y agregarlas todas para que sean observadas por este
IntersectionObserver
;
La parte interesante ocurre dentro de la función de devolución de llamada invocada en las entradas. Hay tres pasos esenciales involucrados.
En primer lugar, procesamos solo los elementos que realmente se cruzan con nuestro "marco de captura". Este fragmento ya debería resultarle familiar.
entries.forEach(entry => { if (entry.isIntersecting) { … } });
Luego, de alguna manera procesamos la entrada al convertir nuestra imagen con
data-src
en un<img src="…">
real.if (entry.isIntersecting) { preloadImage(entry.target); … }
preloadImage()
es una función muy simple que no vale la pena mencionar aquí. Solo lee la fuente.Siguiente y último paso: dado que la carga diferida es una acción única y no necesitamos descargar la imagen cada vez que el elemento entra en nuestro "marco de captura", debemos dejar de
unobserve
la imagen ya procesada. De la misma manera que deberíamos hacerlo conelement.removeEventListener()
para nuestros eventos regulares cuando ya no sean necesarios para evitar pérdidas de memoria en nuestro código.if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as
self
to our callback self.unobserve(entry.target); }
Nota. En lugar de unobserve(event.target)
también podríamos llamar a disconnect()
: desconecta por completo nuestro IntersectionObserver
y ya no observaría las imágenes. Esto es útil si lo único que le importa es el primer hit de su Observer. En nuestro caso, necesitamos que el Observer siga monitoreando las imágenes, por lo que no debemos desconectarnos todavía.
Siéntase libre de bifurcar el ejemplo y jugar con diferentes configuraciones y opciones. Sin embargo, hay una cosa interesante que mencionar cuando desea cargar de forma diferida las imágenes en particular. ¡Siempre debe tener en cuenta la caja generada por el elemento observado! Si revisa el ejemplo, notará que el CSS para imágenes en las líneas 41–47 contiene estilos supuestamente redundantes, incl. min-height: 100px
. Esto se hace para dar a los marcadores de posición de imagen ( <img>
sin atributo src
) alguna dimensión vertical. ¿Para qué?
- Sin dimensiones verticales, todas las etiquetas
<img>
generarían un cuadro de 0×0; - Dado que la etiqueta
<img>
genera algún tipo de cuadro deinline-block
de forma predeterminada, todos esos cuadros de 0 × 0 se alinearían uno al lado del otro en la misma línea; - Esto significa que su
IntersectionObserver
registraría todas (o, dependiendo de qué tan rápido se desplace, casi todas) las imágenes a la vez, probablemente no exactamente lo que desea lograr.
Resaltado de la sección actual
IntersectionObserver
es mucho más que carga diferida, por supuesto. Aquí hay otro ejemplo de cómo reemplazar el evento de scroll
con esta tecnología. En este tenemos un escenario bastante común: en la barra de navegación fija debemos resaltar la sección actual según la posición de desplazamiento del documento.
Estructuralmente, es similar al ejemplo de imágenes de carga diferida y tiene la misma estructura base con las siguientes excepciones:
- Ahora queremos observar no las imágenes, sino las secciones de la página;
- Obviamente, también tenemos una función diferente para procesar las entradas en nuestra devolución de llamada (
intersectionHandler(entry)
). Pero este no es interesante: todo lo que hace es alternar la clase CSS.
Sin embargo, lo que es interesante aquí es el objeto de config
:
const config = { rootMargin: '-50px 0px -55% 0px' };
¿Por qué no el valor predeterminado de 0px
para rootMargin
, te preguntarás? Bueno, simplemente porque resaltar la sección actual y la carga diferida de una imagen son bastante diferentes en lo que intentamos lograr. Con la carga diferida, queremos comenzar a cargar antes de que la imagen entre en la vista. Por lo tanto, para ese propósito, extendimos nuestro "marco de captura" en 50px en la parte inferior. Por el contrario, cuando queremos resaltar la sección actual, tenemos que estar seguros de que la sección es realmente visible en la pantalla. Y no solo eso: tenemos que estar seguros de que el usuario está, en realidad, leyendo o va a leer exactamente esta sección. Por lo tanto, queremos que una sección abarque un poco más de la mitad de la ventana gráfica desde la parte inferior antes de que podamos declararla la sección activa. Además, queremos tener en cuenta la altura de la barra de navegación, por lo que eliminamos la altura de la barra del "marco de captura".
Además, tenga en cuenta que en caso de resaltar el elemento de navegación actual, no queremos dejar de observar nada. Aquí siempre debemos mantener a IntersectionObserver
a cargo, por lo tanto, aquí no encontrará disconnect()
ni unobserve()
.
Resumen
IntersectionObserver
es una tecnología muy sencilla. Tiene un soporte bastante bueno en los navegadores modernos y si desea implementarlo para los navegadores que aún (o no) lo admiten, por supuesto, hay un polyfill para eso. Pero en general, esta es una gran tecnología que nos permite hacer todo tipo de cosas relacionadas con la detección de elementos en una ventana gráfica mientras ayuda a lograr un aumento de rendimiento realmente bueno.
¿Por qué IntersectionObserver es bueno para usted?
- ¡
IntersectionObserver
es una API asíncrona sin bloqueo! -
IntersectionObserver
reemplaza a nuestros costosos oyentes en eventos descroll
o cambio deresize
. -
IntersectionObserver
hace todos los cálculos costosos comogetClientBoundingRect()
por usted para que no tenga que hacerlo. -
IntersectionObserver
sigue el patrón estructural de otros observadores, por lo que, en teoría, debería ser fácil de entender si está familiarizado con el funcionamiento de otros observadores.
Cosas a tener en cuenta
Si comparamos las capacidades de IntersectionObserver con el mundo de window.addEventListener('scroll')
de donde proviene todo, será difícil ver alguna desventaja en este Observer. Entonces, observemos algunas cosas a tener en cuenta en su lugar:
- Sí,
IntersectionObserver
es una API asíncrona sin bloqueo. ¡Es genial saberlo! Pero es aún más importante comprender que el código que está ejecutando en sus devoluciones de llamada no se ejecutará de forma asíncrona de forma predeterminada, aunque la propia API sea asíncrona. Por lo tanto, todavía existe la posibilidad de eliminar todos los beneficios que obtiene deIntersectionObserver
si los cálculos de su función de devolución de llamada hacen que el hilo principal no responda. Pero esta es una historia diferente. - Si está utilizando
IntersectionObserver
para la carga diferida de los activos (como imágenes, por ejemplo), ejecute.unobserve(asset)
después de que se haya cargado el activo. IntersectionObserver
puede detectar intersecciones solo para los elementos que aparecen en la estructura de formato del documento. Para que quede claro: los elementos observables deben generar un cuadro y afectar de alguna manera el diseño. Aquí hay algunos ejemplos para darle una mejor comprensión:- Elementos con
display: none
está descartado; -
opacity: 0
ovisibility:hidden
cree el cuadro (aunque sea invisible) para que estos sean detectados; - Elementos absolutamente posicionados con
width:0px; height:0px
width:0px; height:0px
están bien. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negativetop
,left
, etc.) and are cut out by parent'soverflow: hidden
won't be detected: their box is out of scope for the formatting structure.
- Elementos con
I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:
- Intersection Observer API on MDN;
- IntersectionObserver polyfill;
- IntersectionObserver polyfill as
npm
module; - Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
- Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.
With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.