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

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

Es una de esas APIs que probablemente se agradeció en todo el mundo y, ahora que Safari también lo admite, puede usarse de manera universal en todos los navegadores principales. Para hacer un repaso rápido de la API, te recomendamos mirar Supercharged Microtip de Surma en Intersection Observaer v1 que se incorpora a continuación. También puedes leer el artículo detallado de Surma. Las personas usaron 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 información, consulta los documentos de Intersection Observer sobre MDN, pero 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'));

¿Cuáles son los desafíos de Intersection Observer v1?

Para ser claros, Intersection Observer v1 es excelente, pero no perfecto. Hay algunos casos excepcionales en los que la API falla. Analicemos este tema con más detalle. La API de Intersection Observer v1 puede indicarte cuándo se desplaza un elemento por el viewport de la ventana, pero no te indica si el elemento está cubierto por algún otro contenido de la página (es decir, cuando está oculto) ni si la visualización visual del elemento se modificó con efectos visuales como transform, opacity, filter, etc., que efectivamente 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 con DocumentOrShadowRoot.elementFromPoint() y luego haciendo un análisis más detallado. Por el contrario, no se puede obtener la misma información si el elemento en cuestión se encuentra en un iframe de terceros.

¿Por qué es tan importante la visibilidad real?

Desafortunadamente, Internet es un lugar que atrae a personas que actúan de mala fe con peores intenciones. Por ejemplo, a un publicador sospechoso que publica anuncios de pago por clic en un sitio de contenido se lo podría incentivar a que engañe a las personas para que hagan clic en sus anuncios a fin de aumentar el pago de anuncios del publicador (al menos durante un período breve, hasta que la red de publicidad los detecte). Por lo general, estos anuncios se publican en iframes. Ahora, si el publicador quisiera que los usuarios hagan clic en esos anuncios, puede hacer que los iframes de los anuncios sean completamente transparentes. Para ello, debe aplicar una regla de CSS iframe { opacity: 0; } y superponer los iframes sobre algo atractivo, como un video de un gato adorable en el que los usuarios realmente quieran hacer clic. Esto se denomina captura de clic. Puedes ver este 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"). Verás que el anuncio del iframe "cree" que recibió clics legítimos, aunque era completamente transparente cuando hiciste clic en él (de forma involuntaria).

Engañar a un usuario para que haga clic en un anuncio, diséñalo de manera transparente y colocándolo sobre algo atractivo.

¿Cómo corrige Intersection Observer v2 esto?

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

Un detalle importante de la spec es que la implementación puede informar falsos negativos (es decir, establecer isVisible en false, incluso cuando el elemento de destino es completamente visible y sin modificar). Por motivos de rendimiento y 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, no se permiten los falsos positivos bajo ninguna circunstancia (es decir, establecer isVisible en true cuando el elemento de destino no es completamente visible ni sin modificar).

¿Cómo se ve el nuevo código 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. El 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, se requiere que delay tenga, al menos, 100 (es decir, no más de una notificación cada 100 ms). Como se mencionó antes, es costoso calcular la visibilidad, y este requisito es una precaución contra la degradación del rendimiento y el consumo de batería. El desarrollador responsable usará el valor tolerable más alto para el retraso.

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

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

  • Si el objetivo tiene una matriz de transformación efectiva que no es una traducción 2D o un escalamiento vertical proporcional en 2D, el objetivo se considera invisible.

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

  • Si el objetivo, o cualquier elemento en su cadena de bloques que lo contiene, tiene filtros aplicados, el objetivo se considera invisible.

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

Esto significa que las implementaciones actuales son bastante conservadoras y garantiza 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á que el elemento sea invisible.

A continuación, se ofrece un ejemplo de código corto 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 demostración (pero ahora, intenta “mirar” el video sobre el cachorro). Asegúrate de activar el "modo engañoso" de nuevo para convertirte de inmediato en un publicador sospechoso y ver cómo Intersection Observer v2 evita que se realice un seguimiento de clics en anuncios no legítimos. Esta vez, Intersection Observer v2 nos respalda. 🎉

Intersection Observer v2 evita un clic no intencionado 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

Gracias a Simeon Vincent, Yoav Weiss y Mathias Bynens por revisar este artículo, así como a Stefan Zager por revisar e implementar la función en Chrome. Hero image de Sergey Semin en Unsplash.