Now You See Me: cómo diferir, cargar de forma diferida y actuar con IntersectionObserver

Publicado: 2022-03-10
Resumen rápido ↬ La información de intersección es necesaria por muchas razones, como la carga diferida de imágenes. Pero hay mucho más. Es hora de obtener una mejor comprensión y diferentes perspectivas sobre la API Intersection Observer. ¿Listo?

É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.

un desarrollador web
El desarrollador web ficticio

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.

¡Más después del salto! Continúe leyendo a continuación ↓

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.

IntersectionObserver: ahora me ves

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.

IntersectionObserver y PerformanceObserver son los nuevos miembros de la familia Observers.
IntersectionObserver y PerformanceObserver son los nuevos miembros de la familia Observers.

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.

Observador vs. Evento: ¿cuál es la diferencia?
Observador vs Evento: ¿Cuál es la diferencia?

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.

Los observadores no pretenden reemplazar los eventos: ambos pueden vivir juntos felices.
Los observadores no pretenden reemplazar los eventos: ambos pueden vivir juntos felices.

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

Deconstruyendo 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, la root es la ventana gráfica de su navegador, pero en realidad puede ser cualquier elemento en su DOM (luego configura root en algo como document.getElementById('your-element') ). Sin embargo, tenga en cuenta que los elementos que desea observar deben "vivir" en el árbol DOM de root en este caso.
propiedad de root de la configuración de IntersectionObserver
La propiedad root define la base para 'capturar marco' para nuestros elementos.
  • rootMargin : define el margen alrededor de su elemento root que extiende o reduce el "marco de captura" cuando las dimensiones de su root no brindan suficiente flexibilidad. Las opciones para los valores de esta configuración son similares a las de margin en CSS, como rootMargin: '50px 20px 10px 40px' (arriba, abajo a la derecha, a la izquierda). Los valores se pueden abreviar (como rootMargin: '50px' ) y se pueden expresar en px o % . Por defecto, rootMargin: '0px' .
propiedad rootMargin de la configuración de IntersectionObserver
La propiedad rootMargin expande/contrae el 'marco de captura' que está definido por root .
  • 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 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 del threshold (sé que a veces puede ser confuso), aquí hay algunos ejemplos:
    • threshold: 0 : el valor predeterminado IntersectionObserver 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 que IntersectionObserver 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 .
propiedad de umbral de la configuración de IntersectionObserver
La propiedad de threshold define cuánto debe intersectar el elemento nuestro 'marco de captura' antes de que se dispare el observador.

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 de root .
  • 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.
Captura de pantalla de DevTools con IntersectionObserver activado para todos los elementos a la vez.
IntersectionObserver se activará para todos los elementos observados una vez que estén registrados, pero eso no significa que todos intersecan nuestro 'marco de captura'.

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.
Rectángulos de IntersectionObserverEntry
Todos los rectángulos delimitadores involucrados en IntersectionObserverEntry se calculan automáticamente.

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 es true , entonces el elemento está entrando en el "marco de captura";
  • Si es lo contrario y es false ahora mientras que antes era true , 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.

¿No te parece familiar ya? Sí, la propiedad <code>intersectionRatio</code> es similar a la propiedad <code>threshold</code> de la configuración de Observer. La diferencia es que este último define <em>cuándo</em> iniciar Observer, el primero indica la situación real de la intersección (que es ligeramente diferente de <code>threshold</code> debido a la naturaleza asíncrona de Observer).
¿No te parece familiar ya? Sí, la propiedad intersectionRatio es similar a la propiedad de threshold de la configuración de Observer. La diferencia es que este último define *cuándo* activar Observer, el primero indica la situación real de la intersección (que es ligeramente diferente del threshold debido a la naturaleza asíncrona de Observer).

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.

Carrusel debajo de la primera pantalla de su aplicación
Cuando tenemos un carrusel o cualquier otra funcionalidad de trabajo pesado debajo del pliegue de nuestra aplicación, es una pérdida de recursos comenzar a arrancarla/cargarla de inmediato.

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.

Imágenes de carga diferida debajo del pliegue
Activos de carga diferida como imágenes ubicadas debajo de la primera pantalla: la aplicación más obvia de IntersectionObserver.

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.

Vea la carga Pen Lazy en IntersectionObserver por Denys Mishunov (@mishunov) en CodePen.

Vea la carga Pen Lazy en IntersectionObserver por Denys Mishunov (@mishunov) en CodePen.

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.

  1. 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) { … } });

  2. 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); … }
    Esto hará que el navegador finalmente descargue la imagen. preloadImage() es una función muy simple que no vale la pena mencionar aquí. Solo lee la fuente.

  3. 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 con element.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 de inline-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.

Consulte la sección actual Resaltado del lápiz en IntersectionObserver por Denys Mishunov (@mishunov) en CodePen.

Consulte la sección actual Resaltado del lápiz en IntersectionObserver por Denys Mishunov (@mishunov) en CodePen.

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".

Fotograma de captura para la sección actual
Queremos que el Observador solo detecte elementos que entren en el 'marco de captura' entre 50 px desde la parte superior y el 55 % de la ventana gráfica desde la parte inferior.

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 de scroll o cambio de resize .
  • IntersectionObserver hace todos los cálculos costosos como getClientBoundingRect() 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 de IntersectionObserver 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 o visibility: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 negative top , left , etc.) and are cut out by parent's overflow: hidden won't be detected: their box is out of scope for the formatting structure.
IntersectionObserver: Now You See Me
IntersectionObserver: Now You See Me

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.