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

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

O Intersection Observer v1 é uma daquelas APIs que provavelmente são universalmente adoradas. Agora que o Safari também oferece suporte, ele finalmente pode ser usado 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 ver todos os detalhes, consulte os documentos do Intersection Observer no MDN. Mas, como um breve lembrete, a API Intersection Observer v1 fica assim 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?

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

Incentivar um usuário a clicar em um anúncio usando um estilo transparente e sobrepondo-o em 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, a interseção das instâncias de IntersectionObserverEntry 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 será o novo código na prática?

O construtor IntersectionObserver agora usa mais duas propriedades de configuração: 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 booleano que indica se o observador vai rastrear as mudanças na visibilidade de um alvo.

É 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 alvo 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 proporcional 2D, 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, o destino 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 com a garantia de 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á de volta. 🎉

Intersection Observer v2 impedindo 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.