Confiar é bom, observar é melhor: Intersection Observer v2

Browser Support

  • Chrome: 51.
  • Edge: 15.
  • Firefox: 55.
  • Safari: 12.1.

Source

O Intersection Observer é uma dessas APIs que provavelmente são amadas universalmente e podem ser usadas em todos os principais navegadores. Os desenvolvedores usaram essa API para uma ampla variedade de casos de uso, incluindo carregamento lento de imagens e vídeos, notificações quando os elementos atingem position: sticky, acionamento de eventos de análise e muito mais.

Na forma mais básica, esta é a aparência da API Intersection Observer v1:

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

Desafios de visibilidade no Intersection Observer v1

Com a API Intersection Observer v1, é possível saber quando um elemento é rolado para a janela de visualização. No entanto, não é possível determinar se esse elemento está coberto por outro conteúdo da página, o que é chamado de oclusão, ou se o elemento aparece modificado por CSS, como transform, opacity ou filter, o que pode torná-lo invisível.

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

Por que a visibilidade é importante?

Infelizmente, a Internet tem pessoas mal-intencionadas. Por exemplo, um editor desonesto pode usar anúncios de pagamento por clique em um site. Eles podem enganar os usuários para que cliquem nesses anúncios e ganhem mais dinheiro, pelo menos até que a rede de publicidade descubra o esquema. Normalmente, esses anúncios são veiculados em iframes.

Para enganar os usuários, o editor pode tornar os iframes de anúncios completamente transparentes com CSS: iframe { opacity: 0; }. Em seguida, eles podem colocar esses iframes transparentes sobre conteúdo atraente, como um vídeo de um gatinho fofo, que os usuários querem clicar. Isso é chamado de clickjacking.

Você pode ver um ataque de clickjacking em ação na seção superior da nossa demonstração. Tente "assistir" ao vídeo do gato e ative o modo de truque. O anúncio no iframe registra cliques como legítimos, mesmo que você tenha clicado nele sem querer enquanto o iframe estava transparente.

Enganar um usuário para que ele clique em um anúncio, tornando-o transparente e sobrepondo-o a algo atraente.

Melhorias no Intersection Observer v2

O Intersection Observer v2 pode rastrear a "visibilidade" de um elemento como um humano a definiria. Se você definir uma opção no construtor IntersectionObserver, as instâncias IntersectionObserverEntry resultantes incluem um novo campo booleano chamado isVisible. Quando isVisible é true, o navegador garante que o elemento não esteja coberto por outro conteúdo e não tenha efeitos visuais que ocultem ou mudem a exibição. Se isVisible for false, o navegador não poderá fazer essa garantia.

A especificação permite falsos negativos: isVisible pode ser false mesmo quando o elemento está realmente visível e inalterado. Para melhorar o desempenho, os navegadores usam cálculos mais simples, como caixas delimitadoras e formas retangulares, e não verificam todos os pixels em busca de detalhes complexos, como border-radius.

No entanto, não são permitidos falsos positivos em nenhuma circunstância. Isso significa que isVisible não será true se o elemento não estiver completamente visível e sem modificações.

Aplicar essas mudanças

O construtor IntersectionObserver agora usa mais duas propriedades de configuração:

  • delay é um número que indica o atraso mínimo em milissegundos entre notificações do observador para um determinado destino.
  • trackVisibility é um booleano para indicar se o observador vai rastrear mudanças na visibilidade de um destino.

Quando trackVisibility é true, delay precisa ser definido como 100 ou um valor maior (ou seja, não mais de uma notificação a cada 100 ms). Como a visibilidade é cara de calcular, essa é uma precaução contra a degradação do desempenho e o consumo da bateria. Os desenvolvedores responsáveis devem usar o maior valor tolerável para o atraso.

A especificação calcula a visibilidade. Assim como na versão 1, quando o atributo trackVisibility do observador é false, o destino é considerado visível.

Na versão 2, o destino é considerado invisível se:

  • Ela tem uma matriz de transformação efetiva, que não seja uma translação 2D ou um aumento proporcional 2D.

  • O destino ou qualquer elemento na cadeia de blocos que o contém tem uma opacidade efetiva menor que 1,0.

  • O destino ou qualquer elemento na cadeia de blocos que o contém tem filtros aplicados.

  • Se a implementação não puder garantir que o destino não esteja completamente obstruído por outro conteúdo da página.

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

Confira um exemplo de código 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. Tente "assistir" ao vídeo do cachorrinho. Ative o modo de truque para se transformar em um usuário de má-fé e ver como o Intersection Observer v2 impede que cliques em anúncios não legítimos sejam rastreados. O Intersection Observer v2 nos protege.

O Intersection Observer v2 evita um clique acidental 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 v2, fallback 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'));

Outros recursos

Agradecimentos

Agradecemos a Simeon Vincent, Yoav Weiss e Mathias Bynens pela revisão, além de Stefan Zager pela revisão e implementação do recurso no Chrome.