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

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

Intersection Observer v1 é uma daquelas APIs que provavelmente é muito adotada por todos. Agora que O Safari também é compatível com esse recurso, ele também pode ser usado em todos os principais navegadores. Para relembrar brevemente a API, recomendo assistir os vídeos do Surma Microponta supercarregada em intersecções Observador v1 incorporado abaixo. Você também pode ler o artigo detalhado de Surma artigo. As pessoas já usaram o Intersection Observer v1 em uma ampla variedade de casos de uso, como carregamento lento de imagens e vídeos; receber notificações quando os elementos chegarem a position: sticky; disparar eventos do Analytics, e muito mais.

Para saber todos os detalhes, confira a Documentos do Intersection Observer sobre MDN, mas como um breve lembrete, a API Intersection Observer v1 é parecida com a caso 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'));

Quais são as dificuldades do Intersection Observer v1?

Para deixar claro, Intersection Observer v1 é ótimo, mas não é perfeito. Existem alguns casos extremos em que a API não é suficiente. Vamos dar uma olhada mais de perto! A API Intersection Observer v1 informa quando um elemento é rolado até a seção janela de visualização 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 pela a exibição visual do elemento foi modificada por efeitos visuais como transform, opacity, filter etc., o que pode efetivamente torná-lo invisível.

Para um elemento do documento de nível superior, essas informações podem ser determinadas pela análise o DOM via JavaScript, por exemplo, por meio do DocumentOrShadowRoot.elementFromPoint() e, em seguida, ir mais fundo. Em contraste, as mesmas informações não podem ser obtidas se o elemento em questão for 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 piores. Por exemplo, um editor suspeito que veicula anúncios de pagamento por clique em um site de conteúdo pode ser incentivado para induzir as pessoas a clicar em seus anúncios a fim de aumentar o pagamento de anúncios do editor (pelo menos por um curto período, até que a rede de publicidade os capture). Normalmente, esses anúncios são exibidos em iframes. Agora, se o editor quisesse que os usuários clicassem em tais anúncios, ele poderia tornar os iframes de anúncio completamente transparente aplicando uma regra CSS iframe { opacity: 0; } e sobrepondo os iframes. em cima de algo atraente, como um vídeo fofo de gato em que os usuários realmente querem clicar. Isso é chamado de clickjacking. Você pode ver esse ataque de clickjacking em ação na seção superior deste demo (tente "assistir" o vídeo do gato) e ative o "modo travessura"). Você notará que o anúncio no iframe "pensa" recebeu cliques legítimos, mesmo que tenha sido completamente transparente quando você clicou nele (finge ser involuntariamente).

Induzir 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?

Intersection Observer v2 apresenta o conceito de rastreamento da "visibilidade" real de um alvo elemento que um ser humano o definiria. Ao definir uma opção no construtor IntersectionObserver, se cruzando IntersectionObserverEntry instâncias conterá um novo campo booleano chamado isVisible. Um valor true para isVisible é uma forte garantia da implementação que o elemento de destino esteja completamente desobstruído por outro conteúdo e não tem efeitos visuais que possam alterar ou distorcer a exibição na tela. Por outro lado, um valor false significa que a implementação não pode garantir essa garantia.

Um detalhe importante da spec (link em inglês) é que a implementação tem permissão para informar falsos negativos (ou seja, definir isVisible como false, mesmo quando o elemento de destino está completamente visível e inalterado). Por motivos de desempenho ou por outros motivos, os navegadores se limitam a trabalhar com delimitadores. caixas 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, configurar isVisible como true quando o elemento de destino não está completamente visível e não 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 da o observador de um determinado alvo. O trackVisibility é um booleano que indica se o observador rastreará as mudanças na meta visibilidade.

É importante observar que quando trackVisibility for true, delay precisará estar em no mínimo 100 (ou seja, no máximo uma notificação a cada 100 ms). Como observado antes, 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 vai usar maior valor tolerável para atraso.

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

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

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

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

  • Se o destino, ou qualquer elemento na cadeia de bloqueio que o contém, tiver filtros aplicados, o alvo é considerado invisível.

  • Se a implementação não puder garantir que a segmentação não seja totalmente obstruída por outra página o alvo 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.

Veja abaixo um breve exemplo de código que ilustra os novos recursos da API. É possível conferir o rastreamento de cliques em ação na segunda seção da demonstração (mas agora, tente "assistir" ao vídeo do cachorrinho). Não se esqueça de ativar o "modo de truques" de novo para imediatamente Transforme-se em um editor suspeito e veja como o Intersection Observer v2 que cliques não legítimos sejam acompanhados. 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 Stefan Zager para revisar e implementar o recurso no Chrome. Imagem principal de Sergey Semin no Unsplash.