Confiança é boa, observação é melhor: Intersection Observer v2

O Intersection Observer v2 adiciona a capacidade de não apenas observar interseções em si, mas também detectar se o elemento de interseção estava visível no momento da interseção.

A Intersection Observer v1 é uma das APIs que provavelmente são universalmente adoradas. Agora que o Safari também oferece suporte a ela, ela finalmente pode ser usada em todos os principais navegadores. Para relembrar rapidamente a API, recomendo assistir o Supercharged Microtip de Surma no Intersection Observer v1 incorporado abaixo. Você também pode ler o artigo detalhado de Surma. As pessoas usaram o Intersection Observer v1 para uma ampla gama de casos de uso, como carregamento lento de imagens e vídeos, notificações quando os elementos atingem position: sticky, eventos de análise acionados e muito mais.

Para conferir todos os detalhes, consulte a documentação do Intersection Observer no MDN. Mas, como lembrete, a API Intersection Observer v1 tem a seguinte aparência no caso mais 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'));

Qual é o desafio com o Intersection Observer v1?

Para deixar claro, o Intersection Observer v1 é ótimo, mas não é perfeito. Há alguns casos em que a API não funciona. Vamos dar uma olhada mais de perto. A API Intersection Observer v1 pode informar quando um elemento é rolado para a viewport da janela, mas não informa se o elemento está coberto por qualquer outro conteúdo da página (ou seja, quando o elemento está obstruído) ou se a exibição visual do elemento foi modificada por efeitos visuais, como transform, opacity, filter etc., que efetivamente podem torná-lo invisível.

Para um elemento no documento de nível superior, essas informações podem ser determinadas analisando o DOM por JavaScript, por exemplo, usando DocumentOrShadowRoot.elementFromPoint() e depois aprofundando. Por outro lado, não é possível acessar as mesmas informações se o elemento em questão estiver localizado em um iframe de terceiros.

Por que a visibilidade real é tão importante?

Infelizmente, a Internet é um lugar que atrai usuários de má-fé com intenções ainda piores. Por exemplo, um editor duvidoso que veicula anúncios PPC em um site de conteúdo pode ser incentivado a enganar as pessoas para que cliquem nos anúncios e aumentem o pagamento do editor (pelo menos por um curto período, até que a rede de publicidade os detecte). Normalmente, esses anúncios são veiculados em iframes. Agora, se o editor quisesse fazer com que os usuários clicassem nesses anúncios, ele poderia tornar os iframes de anúncios completamente transparentes aplicando uma regra CSS iframe { opacity: 0; } e sobrepondo os iframes em cima de algo atraente, como um vídeo de gato fofo que os usuários realmente gostariam de clicar. Isso é chamado de clickjacking. É possível conferir um ataque de clickjacking na seção de cima desta demonstração (tente "assistir" o vídeo do gato e ativar o "modo truque"). Você vai notar que o anúncio no iframe "acha" que recebeu cliques legítimos, mesmo que ele estivesse completamente transparente quando você clicou nele (fingindo ser involuntário).

Enganar um usuário para que clique em um anúncio estilizando-o com transparência e sobrepondo-o a algo atraente.

Como o Intersection Observer v2 corrige isso?

O Intersection Observer v2 introduz o conceito de rastreamento da "visibilidade" real de um elemento alvo, como um ser humano o definiria. Ao definir uma opção no construtor IntersectionObserver, as instâncias IntersectionObserverEntry que se cruzam vão conter um novo campo booleano chamado isVisible. Um valor true para isVisible é uma garantia forte da implementação de que o elemento de destino não é totalmente ocultado por outro conteúdo e não tem efeitos visuais aplicados que alterem ou distorçam a exibição na tela. Por outro lado, um valor false significa que a implementação não pode fazer essa garantia.

Um detalhe importante da especificação é que a implementação é permitida para informar falsas negativas, ou seja, definir isVisible como false, mesmo quando o elemento de destino está completamente visível e não modificado. Por motivos de desempenho ou outros, os navegadores se limitam a trabalhar com caixas limitantes e geometria retilínea. Eles não tentam alcançar resultados perfeitos para modificações como border-radius.

No entanto, falsas correspondências não são permitidas em nenhuma circunstância. Isso significa que isVisible não pode ser definido como true quando o elemento de destino não está completamente visível e não foi modificado.

Como é o novo código na prática?

O construtor IntersectionObserver agora usa duas propriedades de configuração adicionais: delay e trackVisibility. O delay é um número que indica o atraso mínimo em milissegundos entre as notificações do observador para um determinado destino. O trackVisibility é um valor booleano que indica se o observador vai rastrear as mudanças na visibilidade de um destino.

É importante observar que, quando trackVisibility é true, delay precisa ser, pelo menos, 100 (ou seja, não mais de uma notificação a cada 100 ms). Como observado anteriormente, a visibilidade é cara para calcular, e esse requisito é uma precaução contra a degradação de desempenho e o consumo de bateria. O desenvolvedor responsável vai usar o maior valor tolerável para atraso.

De acordo com a especificação atual, a visibilidade é calculada da seguinte maneira:

  • Se o atributo trackVisibility do observador for false, o destino será considerado visível. Isso corresponde ao comportamento atual da v1.

  • Se o alvo tiver uma matriz de transformação efetiva diferente de uma tradução 2D ou aumento de escala 2D proporcional, ele será considerado invisível.

  • Se o destino ou qualquer elemento na cadeia de blocos que o contém tiver uma opacidade efetiva diferente de 1,0, ele será considerado invisível.

  • Se o destino ou qualquer elemento na cadeia de blocos que o contém tiver filtros aplicados, ele será considerado invisível.

  • Se a implementação não puder garantir que o alvo não seja completamente oculto por outro conteúdo da página, ele será considerado invisível.

Isso significa que as implementações atuais são bastante conservadoras ao garantir a visibilidade. Por exemplo, aplicar um filtro de escala de cinza quase imperceptível, como filter: grayscale(0.01%), ou definir uma transparência quase invisível com opacity: 0.99 renderizaria o elemento invisível.

Confira abaixo um exemplo de código curto que ilustra os novos recursos da API. Você pode conferir a lógica de rastreamento de cliques em ação na segunda seção da demonstração (mas agora, tente "assistir" o vídeo do filhote). Ative o "modo de truque" novamente para se tornar imediatamente um editor duvidoso e conferir como o Intersection Observer v2 impede que cliques em anúncios não legítimos sejam rastreados. Desta vez, o Intersection Observer v2 está aqui para ajudar. 🎉

O Intersection Observer v2 impede um clique não intencional em um anúncio.

<!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'));

Agradecimentos

Agradecemos a Simeon Vincent, Yoav Weiss e Mathias Bynens por revisar este artigo, bem como a Stefan Zager por revisar e implementar o recurso no Chrome. Imagem principal de Sergey Semin no Unsplash.