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

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

A Intersection Observer v1 é uma das APIs que provavelmente foi muito usada por todos e, agora que o Safari é compatível, ela também pode ser usada em todos os principais navegadores. Para uma atualização rápida da API, recomendamos assistir a Supercharged Microtip da Surma no Intersection Observer v1 incorporada abaixo. Você também pode ler o artigo detalhado do Surma. As pessoas usaram o Intersection Observer v1 para diversos casos de uso, como carregamento lento de imagens e vídeos, notificações quando os elementos atingem position: sticky, disparo de eventos de análise e muito mais.

Para saber mais detalhes, consulte os documentos do Intersection Observer no MDN, mas como um lembrete rápido, a API Intersection Observer v1 é 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 do Intersection Observer v1?

Para esclarecer, o Intersection Observer v1 é ótimo, mas não é perfeito. Há alguns casos específicos em que a API fica aquém. Vamos conferir mais detalhes. A API Intersection Observer v1 pode informar quando um elemento é rolado até a janela de visualização da janela, mas não informa se ele está coberto por outro conteúdo da página (ou seja, quando está oculto) ou se a exibição visual do elemento foi modificada por efeitos visuais como transform, opacity, filter etc., o que efetivamente pode torná-lo invisível.

Para um elemento no documento de nível superior, essas informações podem ser determinadas analisando o DOM via JavaScript, por exemplo, via DocumentOrShadowRoot.elementFromPoint() e fazendo uma análise mais detalhada. Por outro lado, as mesmas informações não poderão ser obtidas 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 más intenções. Por exemplo, um editor duvidoso que veicula anúncios de pagamento por clique em um site de conteúdo pode ser incentivado a induzir as pessoas a clicar nos anúncios para aumentar o pagamento do anúncio do editor (pelo menos por um curto período, até que a rede de publicidade as capture). Normalmente, tais anúncios são veiculados em iframes. Agora, se o editor quiser que os usuários cliquem nesses anúncios, ele poderia deixar os iframes do anúncio completamente transparentes aplicando uma regra CSS iframe { opacity: 0; } e sobrepondo os iframes sobre algo atraente, como um vídeo de gato fofo em que os usuários realmente clicariam. Isso é chamado de clickjacking. Confira esse ataque de clickjacking em ação na seção superior desta demonstração. Tente "assistir" o vídeo de gato e ative o "modo de truques". Você notará que o anúncio no iframe "acha" que recebeu cliques legítimos, mesmo que tenha sido completamente transparente quando você (finja-involuntariamente) clicou nele.

Induzindo o usuário a clicar em um anúncio deixando-o transparente e sobrepondo-o sobre algo atraente.

Como o Intersection Observer v2 corrige isso?

O Intersection Observer v2 apresenta o conceito de rastrear a "visibilidade" real de um elemento de destino como um ser humano o definiria. Ao definir uma opção no construtor IntersectionObserver, a interseção das instâncias IntersectionObserverEntry conterá um novo campo booleano chamado isVisible. Um valor true para isVisible é uma forte garantia da implementação subjacente de que o elemento de destino não está completamente oculto por outro conteúdo e não tem efeitos visuais aplicados que alterariam ou distorceriam 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 spec é que a implementação tem permissão para informar falsos negativos, ou seja, definir isVisible como false mesmo quando o elemento de destino estiver completamente visível e não modificado. Por motivos de desempenho ou por outros motivos, os navegadores se limitam a trabalhar com caixas delimitadoras e geometria retilínea. Eles não tentam alcançar resultados perfeitos para modificações como border-radius.

Dito isso, falsos positivos não são permitidos em nenhuma circunstância, ou seja, definir isVisible como true quando o elemento de destino não estiver completamente visível e não tiver sido modificado.

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

O construtor IntersectionObserver agora usa duas outras 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 destino.

É importante observar que, quando trackVisibility é true, delay precisa ser pelo menos 100, ou seja, no máximo uma notificação a cada 100 ms. Conforme observado anteriormente, o cálculo da visibilidade é caro, e esse requisito é uma precaução contra degradação do desempenho e consumo da bateria. O desenvolvedor responsável usará o maior valor tolerável para o atraso.

De acordo com a spec atual, a visibilidade é calculada da seguinte forma:

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

  • Se o destino tiver uma matriz de transformação efetiva que não seja uma translação 2D ou um aumento 2D proporcional, o destino será considerado invisível.

  • Se o destino ou qualquer elemento na cadeia de blocos dele tiver uma opacidade efetiva diferente de 1.0, o destino será considerado invisível.

  • Se o destino ou qualquer elemento na cadeia de blocos dele tiver filtros aplicados, o destino será considerado invisível.

  • Se a implementação não puder garantir que o destino não esteja 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 em escala de cinza quase imperceptível, como filter: grayscale(0.01%), ou definir uma transparência quase invisível com opacity: 0.99, tornaria o elemento invisível.

Abaixo está um pequeno exemplo de código que ilustra os novos recursos da API. Veja a lógica de rastreamento de cliques em ação na segunda seção da demonstração. Agora, tente "assistir" o vídeo do filhote. Ative o "modo de truques" novamente para se converter imediatamente em um editor suspeito e veja como o Intersection Observer v2 impede o rastreamento de cliques não legítimos em anúncios. Desta vez, o Intersection Observer v2 voltou! 🎉

Intersection Observer v2, que 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 revisarem este artigo. Também agradecemos a Stefan Zager por analisar e implementar o recurso no Chrome. Imagem principal de Sergey Semin no Unsplash.