La confianza es buena y la observación es mejor: Intersection Observer v2

Intersection Observer v2 agrega la capacidad de no solo observar las intersecciones en sí, sino también detectar si el elemento que se cruzaba era visible en el momento de la intersección.

Intersection Observer v1 es una de esas APIs que probablemente sea del agrado de todos, y ahora que Safari también la admite, por fin se puede usar de forma universal en todos los navegadores principales. Para hacer un repaso rápido de la API, te recomiendo que mires la Microsugerencia superpotente de Surma sobre Intersection Observer v1 que se incorpora a continuación. También puedes leer el artículo detallado de Surma. Se usó Intersection Observer v1 para una amplia variedad de casos de uso, como la carga diferida de imágenes y videos, recibir notificaciones cuando los elementos llegan a position: sticky, activar eventos de estadísticas y muchos más.

Para obtener más detalles, consulta los documentos de Intersection Observer en MDN. Sin embargo, a modo de recordatorio, así se ve la API de Intersection Observer v1 en el caso más básico:

const onIntersection = (entries) => {
  for (const entry of entries) {
    if (entry.isIntersecting) {
      console.log(entry);
    }
  }
};

const observer = new IntersectionObserver(onIntersection);
observer.observe(document.querySelector('#some-target'));

¿Qué dificultades presenta Intersection Observer v1?

Para que quede claro, Intersection Observer v1 es excelente, pero no es perfecto. Existen algunos casos excepcionales en los que la API no es suficiente. Veamos más detalles. La API de Intersection Observer v1 puede indicarte cuándo se desplaza un elemento al viewport de la ventana, pero no te indica si el elemento está cubierto por cualquier otro contenido de la página (es decir, cuando el elemento está ocluido) o si la visualización visual del elemento se modificó con efectos visuales, como transform, opacity, filter, etc., que de manera efectiva pueden hacerlo invisible.

En el caso de un elemento del documento de nivel superior, esta información se puede determinar analizando el DOM a través de JavaScript, por ejemplo, a través de DocumentOrShadowRoot.elementFromPoint() y, luego, profundizando. Por el contrario, no se puede obtener la misma información si el elemento en cuestión está ubicado en un iframe de terceros.

¿Por qué la visibilidad real es tan importante?

Lamentablemente, Internet es un lugar que atrae a personas que actúan de mala fe con intenciones peores. Por ejemplo, un publicador poco confiable que publica anuncios pagados por clic en un sitio de contenido podría tener incentivos para engañar a las personas y que hagan clic en sus anuncios para aumentar los pagos de anuncios del publicador (al menos por un período breve, hasta que la red de publicidad lo detecte). Por lo general, estos anuncios se publican en iframes. Ahora bien, si el publicador quisiera que los usuarios hicieran clic en esos anuncios, podría hacer que los iframes de anuncios sean completamente transparentes aplicando una regla CSS iframe { opacity: 0; } y superponiendo los iframes sobre algo atractivo, como un video de un gato lindo en el que los usuarios realmente quieran hacer clic. Esto se conoce como secuestro de clics. Puedes ver un ataque de clickjacking en acción en la sección superior de esta demostración (intenta "mirar" el video del gato y activa el "modo truco"). Notarás que el anuncio en el iframe "piensa" que recibió clics legítimos, incluso si era completamente transparente cuando hiciste clic en él (de forma simulada e involuntaria).

Engaño a un usuario para que haga clic en un anuncio con un diseño transparente y lo superponga sobre algo atractivo.

¿Cómo se soluciona este problema con Intersection Observer v2?

Intersection Observer v2 presenta el concepto de hacer un seguimiento de la "visibilidad" real de un elemento objetivo como lo definiría un ser humano. Si configuras una opción en el constructor IntersectionObserver, las instancias de IntersectionObserverEntry que se intersecan contendrán un nuevo campo booleano llamado isVisible. Un valor true para isVisible es una garantía sólida de la implementación subyacente de que el elemento de destino no está obstruido por otro contenido y no tiene efectos visuales aplicados que alteren o distorsionen su visualización en la pantalla. Por el contrario, un valor de false significa que la implementación no puede garantizarlo.

Un detalle importante de la especificación es que la implementación tiene permitido informar falsos negativos (es decir, establecer isVisible en false, incluso cuando el elemento de destino está completamente visible y sin modificaciones). Por motivos de rendimiento o por otros motivos, los navegadores se limitan a trabajar con cuadros de límite y geometría rectilínea; no intentan lograr resultados perfectos en píxeles para modificaciones como border-radius.

Dicho esto, los falsos positivos no se permiten bajo ninguna circunstancia (es decir, establecer isVisible en true cuando el elemento de destino no está completamente visible y sin modificaciones).

¿Cómo se ve el código nuevo en la práctica?

El constructor IntersectionObserver ahora toma dos propiedades de configuración adicionales: delay y trackVisibility. delay es un número que indica el retraso mínimo en milisegundos entre las notificaciones del observador para un objetivo determinado. trackVisibility es un valor booleano que indica si el observador hará un seguimiento de los cambios en la visibilidad de un objetivo.

Es importante tener en cuenta que, cuando trackVisibility es true, delay debe ser 100 como mínimo (es decir, no más de una notificación cada 100 ms). Como se señaló antes, el cálculo de la visibilidad es costoso, y este requisito es una precaución contra el deterioro del rendimiento y el consumo de batería. El desarrollador responsable usará el valor tolerable más alto para la demora.

Según la especificación actual, la visibilidad se calcula de la siguiente manera:

  • Si el atributo trackVisibility del observador es false, el objetivo se considera visible. Esto corresponde al comportamiento actual de la versión 1.

  • Si el destino tiene una matriz de transformación eficaz que no sea una traslación 2D o un escalamiento vertical proporcional en 2D, se considerará invisible.

  • Si el objetivo, o cualquier elemento de su cadena de bloques contenedora, tiene una opacidad efectiva distinta de 1.0, el objetivo se considera invisible.

  • Si el objetivo, o cualquier elemento de su cadena de bloques contenedora, tiene algún filtro aplicado, se considera invisible.

  • Si la implementación no puede garantizar que el objetivo no esté completamente oculto por otro contenido de la página, se considera invisible.

Esto significa que las implementaciones actuales son bastante conservadoras y garantizan la visibilidad. Por ejemplo, aplicar un filtro de escala de grises casi imperceptible, como filter: grayscale(0.01%), o configurar una transparencia casi invisible con opacity: 0.99, haría que el elemento fuera invisible.

A continuación, se ofrece una breve muestra de código que ilustra las nuevas funciones de la API. Puedes ver su lógica de seguimiento de clics en acción en la segunda sección de la demo (pero ahora, intenta “mirar” el video del cachorro). Asegúrate de activar el "modo truco" nuevamente para convertirte de inmediato en un publicador sospechoso, y observa cómo Intersection Observer v2 evita que se haga un seguimiento de clics en anuncios no legítimos. Esta vez, Intersection Observer v2 nos ayuda. 🎉

Intersection Observer v2 evita un clic no deseado en un anuncio.

<!DOCTYPE html>
<!-- This is the ad running in the iframe -->
<button id="callToActionButton">Buy now!</button>
// This is code running in the iframe.

// The iframe must be visible for at least 800ms prior to an input event
// for the input event to be considered valid.
const minimumVisibleDuration = 800;

// Keep track of when the button transitioned to a visible state.
let visibleSince = 0;

const button = document.querySelector('#callToActionButton');
button.addEventListener('click', (event) => {
  if ((visibleSince > 0) &&
      (performance.now() - visibleSince >= minimumVisibleDuration)) {
    trackAdClick();
  } else {
    rejectAdClick();
  }
});

const observer = new IntersectionObserver((changes) => {
  for (const change of changes) {
    // ⚠️ Feature detection
    if (typeof change.isVisible === 'undefined') {
      // The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
      change.isVisible = true;
    }
    if (change.isIntersecting && change.isVisible) {
      visibleSince = change.time;
    } else {
      visibleSince = 0;
    }
  }
}, {
  threshold: [1.0],
  // 🆕 Track the actual visibility of the element
  trackVisibility: true,
  // 🆕 Set a minimum delay between notifications
  delay: 100
}));

// Require that the entire iframe be visible.
observer.observe(document.querySelector('#ad'));

Agradecimientos

Agradecemos a Simeon Vincent, Yoav Weiss y Mathias Bynens por leer este artículo, y a Stefan Zager también por revisar y por implementar la función en Chrome. Imagen hero de Sergey Semin en Unsplash.