Agora você me vê: como adiar, carregar lentamente e agir com o IntersectionObserver

Publicados: 2022-03-10
Resumo rápido ↬ As informações de interseção são necessárias por vários motivos, como carregamento lento de imagens. Mas há muito mais. É hora de obter uma melhor compreensão e diferentes perspectivas sobre a API Intersection Observer. Preparar?

Era uma vez um desenvolvedor web que convenceu com sucesso seus clientes de que os sites não deveriam ter a mesma aparência em todos os navegadores, se preocupava com a acessibilidade e foi um dos primeiros a adotar grades CSS. Mas no fundo de seu coração era o desempenho que era sua verdadeira paixão: ele constantemente otimizava, minificava, monitorava e até empregava truques psicológicos em seus projetos.

Então, um dia, ele aprendeu sobre o carregamento lento de imagens e outros ativos que não são imediatamente visíveis para os usuários e não são essenciais para renderizar conteúdo significativo na tela. Era o começo do amanhecer: o desenvolvedor entrou no mundo maligno dos plugins jQuery de carregamento lento (ou talvez no mundo não tão ruim dos atributos async e defer ). Alguns até dizem que ele foi direto ao cerne de todos os males: o mundo dos ouvintes de eventos de scroll . Nunca saberemos ao certo onde ele foi parar, mas novamente este desenvolvedor é absolutamente fictício, e qualquer semelhança com qualquer desenvolvedor é mera coincidência.

um desenvolvedor web
O desenvolvedor web fictício

Bem, agora você pode dizer que a caixa de Pandora foi aberta e que nosso fictício desenvolvedor não torna o problema menos real. Hoje em dia, priorizar o conteúdo acima da dobra tornou-se extremamente importante para o desempenho de nossos projetos web tanto do ponto de vista da velocidade quanto do peso da página.

Mais depois do salto! Continue lendo abaixo ↓

Neste artigo, vamos sair da escuridão da scroll e falar sobre a maneira moderna de carregar recursos lentamente. Não apenas carregando imagens lentas, mas carregando qualquer ativo para esse assunto. Mais ainda, a técnica sobre a qual falaremos hoje é capaz de muito mais do que ativos de carregamento lento: seremos capazes de fornecer qualquer tipo de funcionalidade diferida com base na visibilidade dos elementos para os usuários.

IntersectionObserver: Agora você me vê

Senhoras e senhores, vamos falar sobre a API Intersection Observer. Mas antes de começarmos, vamos dar uma olhada no panorama das ferramentas modernas que nos levaram ao IntersectionObserver .

2017 foi um ano muito bom para ferramentas incorporadas em nossos navegadores, ajudando-nos a melhorar a qualidade e o estilo de nossa base de código sem muito esforço. Hoje em dia, a web parece estar se afastando de soluções esporádicas baseadas em soluções muito diferentes para soluções muito típicas para uma abordagem mais bem definida de interfaces Observer (ou apenas “Observers”): MutationObserver bem suportado obteve novos membros da família que foram rapidamente adotado em navegadores modernos:

  • IntersectionObserver e
  • PerformanceObserver (como parte da especificação Performance Timeline Level 2).

Mais um membro da família em potencial, FetchObserver, é um trabalho em andamento e nos guia mais para as terras de um proxy de rede, mas hoje eu gostaria de falar mais sobre front-end.

IntersectionObserver e PerformanceObserver são os novos membros da família Observers.
IntersectionObserver e PerformanceObserver são os novos membros da família Observers.

PerformanceObserver e IntersectionObserver visam ajudar desenvolvedores front-end a melhorar o desempenho de seus projetos em diferentes pontos. O primeiro nos dá a ferramenta para o Real User Monitoring, enquanto o segundo é a ferramenta, proporcionando-nos uma melhoria de desempenho tangível. Como mencionado anteriormente, este artigo analisará detalhadamente exatamente o último: IntersectionObserver . Para entender a mecânica do IntersectionObserver em particular, devemos dar uma olhada em como um Observer genérico deve funcionar na web moderna.

Dica profissional : Você pode pular a teoria e mergulhar na mecânica do IntersectionObserver imediatamente ou, ainda mais, direto para as possíveis aplicações do IntersectionObserver .

Observador vs. Evento

Um “Observador”, como o nome indica, destina-se a observar algo que acontece no contexto de uma página. Os observadores podem observar algo acontecendo em uma página, como mudanças no DOM. Eles também podem observar os eventos do ciclo de vida da página. Os observadores também podem executar algumas funções de retorno de chamada. Agora, o leitor atento pode imediatamente identificar o problema aqui e perguntar: “Então, qual é o ponto? Já não temos eventos para este fim? O que torna os Observadores diferentes?” Ponto muito bom! Vamos dar uma olhada mais de perto e resolver isso.

Observador vs. Evento: qual é a diferença?
Observador vs. Evento: Qual é a diferença?

A diferença crucial entre o Event regular e o Observer é que, por padrão, o primeiro reage de forma síncrona para cada ocorrência do Event, afetando a capacidade de resposta do thread principal, enquanto o último deve reagir de forma assíncrona sem afetar muito o desempenho. Pelo menos, isso é verdade para os Observadores apresentados atualmente: Todos eles se comportam de forma assíncrona e não acho que isso mudará no futuro.

Isso leva à principal diferença no tratamento dos retornos de chamada dos Observadores que podem confundir os iniciantes: a natureza assíncrona dos observadores pode resultar em vários observáveis ​​sendo passados ​​para uma função de retorno de chamada ao mesmo tempo. Por causa disso, a função de retorno de chamada não deve esperar uma única entrada, mas um Array de entradas (mesmo que às vezes o Array contenha apenas uma entrada).

Além disso, alguns Observadores (em particular o que estamos falando hoje) fornecem propriedades pré-computadas muito úteis, que, de outra forma, usávamos para nos calcular usando métodos e propriedades caros (do ponto de vista de desempenho) ao usar eventos regulares. Para esclarecer este ponto, vamos chegar a um exemplo um pouco mais adiante no artigo.

Então, se é difícil para alguém se afastar do paradigma do Evento, eu diria que os Observadores são eventos com esteróides. Outra descrição seria: Observadores são um novo nível de aproximação em cima dos eventos. Mas não importa qual definição você prefira, não é preciso dizer que os Observadores não se destinam a substituir eventos (pelo menos ainda não); há casos de uso suficientes para ambos, e eles podem viver lado a lado felizes.

Observadores não pretendem substituir Eventos: Ambos podem viver juntos e felizes.
Observadores não pretendem substituir Eventos: Ambos podem viver juntos e felizes.

Estrutura do Observador Genérico

A estrutura genérica de um Observer (qualquer uma das disponíveis no momento da redação) é semelhante a esta:

 /** * Typical Observer's registration */ let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) { // entries: Array of observed elements entries.forEach(entry => { // Here we can do something with each particular entry }); }); // Now we should tell our Observer what to observe observer.observe(WHAT-TO-OBSERVE);

Novamente, observe que entries são uma Array de valores, não uma única entrada.

Esta é a estrutura genérica: Implementações de Observadores particulares diferem nos argumentos que estão sendo passados ​​para seu observe() e os argumentos passados ​​para seu retorno de chamada. Por exemplo, o MutationObserver também deve obter um objeto de configuração para saber mais sobre quais mudanças no DOM devem ser observadas. PerformanceObserver não observa nós no DOM, mas tem o conjunto dedicado de tipos de entrada que pode observar.

Aqui, vamos terminar a parte “genérica” desta discussão e mergulhar mais fundo no tópico do artigo de hoje — IntersectionObserver .

Desconstruindo o IntersectionObserver

Desconstruindo o IntersectionObserver
Desconstruindo o IntersectionObserver

Primeiro de tudo, vamos descobrir o que é IntersectionObserver .

De acordo com o MDN:

A API Intersection Observer fornece uma maneira de observar de forma assíncrona as alterações na interseção de um elemento de destino com um elemento ancestral ou com a janela de visualização de um documento de nível superior.

Simplificando, IntersectionObserver observa de forma assíncrona a sobreposição de um elemento por outro elemento. Vamos falar sobre para que servem esses elementos no IntersectionObserver .

Inicialização do IntersectionObserver

Em um dos parágrafos anteriores, vimos a estrutura de um Observador genérico. IntersectionObserver estende um pouco essa estrutura. Em primeiro lugar, este tipo de Observador requer uma configuração com três elementos principais:

  • root : Este é o elemento raiz usado para a observação. Ele define o “quadro de captura” básico para elementos observáveis. Por padrão, a root é a janela de visualização do seu navegador, mas pode realmente ser qualquer elemento em seu DOM (depois você define a root como algo como document.getElementById('your-element') ). Tenha em mente que os elementos que você deseja observar devem “viver” na árvore DOM do root neste caso.
propriedade da raiz da configuração do IntersectionObserver
root define a base para 'capturar frame' para nossos elementos.
  • rootMargin : Define a margem em torno de seu elemento root que estende ou reduz o “quadro de captura” quando as dimensões de sua root não fornecem flexibilidade suficiente. As opções para os valores desta configuração são semelhantes às de margin em CSS, como rootMargin: '50px 20px 10px 40px' (superior, inferior direito, esquerdo). Os valores podem ser abreviados (como rootMargin: '50px' ) e podem ser expressos em px ou % . Por padrão, rootMargin: '0px' .
propriedade rootMargin da configuração do IntersectionObserver
rootMargin expande/contrai o 'quadro de captura' que é definido por root .
  • threshold : nem sempre é desejável reagir instantaneamente quando um elemento observado cruza uma borda do “frame de captura” (definido como uma combinação de root e rootMargin ). threshold define a porcentagem de tal interseção na qual o Observer deve reagir. Pode ser definido como um valor único ou como uma matriz de valores. Para entender melhor o efeito do threshold (sei que às vezes pode ser confuso), aqui estão alguns exemplos:
    • threshold: 0 : O valor padrão IntersectionObserver deve reagir quando o primeiro ou o último pixel de um elemento observado cruza uma das bordas do “quadro de captura”. Tenha em mente que o IntersectionObserver é independente de direção, o que significa que ele reagirá em ambos os cenários: a) quando o elemento entrar eb) quando sair do “quadro de captura”.
    • threshold: 0.5 : Observer deve ser acionado quando 50% de um elemento observado cruzar o “frame de captura”;
    • threshold: [0, 0.2, 0.5, 1] ​​: O observador deve reagir em 4 casos:
      • O primeiro pixel de um elemento observado entra no “quadro de captura”: o elemento ainda não está realmente dentro desse quadro, ou o último pixel do elemento observado sai do “quadro de captura”: o elemento não está mais dentro do quadro;
      • 20% do elemento está dentro do “quadro de captura” (novamente, a direção não importa para IntersectionObserver );
      • 50% do elemento está dentro do “quadro de captura”;
      • 100% do elemento está dentro do “quadro de captura”. Isso é estritamente oposto ao threshold: 0 .
propriedade de limite da configuração do IntersectionObserver
A propriedade threshold define quanto o elemento deve cruzar nosso 'quadro de captura' antes que o Observer seja acionado.

Para informar nosso IntersectionObserver sobre nossa configuração desejada, simplesmente passamos nosso objeto de config para o construtor do nosso Observer junto com nossa função de retorno de chamada assim:

 const config = { root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport rootMargin: '0px', threshold: 0.5 }; let observer = new IntersectionObserver(function(entries) { … }, config);

Agora, devemos dar ao IntersectionObserver o elemento real a ser observado. Isso é feito simplesmente passando o elemento para a função observe() :

 … const img = document.getElementById('image-to-observe'); observer.observe(image);

Algumas coisas a serem observadas sobre esse elemento observado:

  • Já foi mencionado anteriormente, mas vale a pena ser mencionado novamente: Caso você defina root como um elemento no DOM, o elemento observado deve estar localizado dentro da árvore DOM de root .
  • IntersectionObserver pode aceitar apenas um elemento para observação por vez e não oferece suporte ao fornecimento em lote para observações. Isso significa que, se você precisar observar vários elementos (digamos, várias imagens em uma página), precisará iterar sobre todos eles e observar cada um deles separadamente:
 … const images = document.querySelectorAll('img'); images.forEach(image => { observer.observe(image); });
  • Ao carregar uma página com o Observer no local, você pode notar que o retorno de chamada do IntersectionObserver foi acionado para todos os elementos observados de uma só vez. Mesmo aqueles que não correspondem à configuração fornecida. "Bem... não é realmente o que eu esperava", é o pensamento usual ao experimentar isso pela primeira vez. Mas não se confunda aqui: isso não significa necessariamente que esses elementos observados de alguma forma cruzam o “quadro de captura” enquanto a página está carregando.
Captura de tela do DevTools com IntersectionObserver sendo acionado para todos os elementos de uma vez.
IntersectionObserver será disparado para todos os elementos observados assim que forem registrados, mas isso não significa que todos eles cruzam nosso 'quadro de captura'.

O que significa, porém, é que a entrada para este elemento foi inicializada e agora é controlada pelo seu IntersectionObserver . No entanto, isso pode adicionar ruído desnecessário à sua função de retorno de chamada, e torna-se sua responsabilidade detectar quais elementos realmente cruzam o “quadro de captura” e quais ainda não precisamos levar em conta. Para entender como fazer essa detecção, vamos nos aprofundar um pouco mais na anatomia de nossa função de retorno de chamada e dar uma olhada no que consistem essas entradas.

Retorno de chamada do IntersectionObserver

Em primeiro lugar, a função de retorno de chamada para um IntersectionObserver recebe dois argumentos, e falaremos sobre eles em ordem inversa começando com o segundo argumento. Juntamente com o já mencionado Array de entradas observadas, cruzando nosso “quadro de captura”, a função de retorno de chamada obtém o próprio Observer como o segundo argumento.

Referência ao próprio observador

 new IntersectionObserver(function(entries, SELF) {…});

Obter a referência ao próprio Observer é útil em muitos cenários quando você deseja parar de observar algum elemento depois que ele foi detectado pelo IntersectionObserver pela primeira vez. Cenários como carregamento lento das imagens, busca adiada de outros ativos etc. são desse tipo. Quando você deseja parar de observar um elemento, IntersectionObserver fornece um método unobserve(element-to-stop-observing) que pode ser executado na função de retorno de chamada após realizar algumas ações no elemento observado (como carregamento lento real de uma imagem, por exemplo ).

Alguns desses cenários serão revisados ​​mais adiante no artigo, mas com este segundo argumento fora do nosso caminho, vamos aos principais atores desta peça de retorno de chamada.

IntersectionObserverEntry

 new IntersectionObserver(function(ENTRIES, self) {…});

As entries que estamos recebendo em nossa função de retorno de chamada como um Array são do tipo especial: IntersectionObserverEntry . Essa interface nos fornece um conjunto pré-definido e pré-calculado de propriedades relativas a cada elemento observado em particular. Vamos dar uma olhada nos mais interessantes.

Em primeiro lugar, as entradas do tipo IntersectionObserverEntry vêm com informações sobre três retângulos diferentes — definindo coordenadas e limites dos elementos envolvidos no processo:

  • rootBounds : Um retângulo para o “quadro de captura” ( root + rootMargin );
  • boundingClientRect : Um retângulo para o próprio elemento observado;
  • intersectionRect : Uma área do “quadro de captura” interceptada pelo elemento observado.
Retângulos de IntersectionObserverEntry
Todos os retângulos delimitadores envolvidos em IntersectionObserverEntry são calculados para você.

A coisa muito legal sobre esses retângulos sendo calculados para nós de forma assíncrona é que ele nos fornece informações importantes relacionadas ao posicionamento do elemento sem getBoundingClientRect() , offsetTop , offsetLeft e outras propriedades e métodos de posicionamento caros que acionam o layout thrashing. Pura vitória pelo desempenho!

Outra propriedade da interface IntersectionObserverEntry que é interessante para nós é isIntersecting . Esta é uma propriedade de conveniência que indica se o elemento observado está cruzando o “quadro de captura” ou não. Poderíamos, é claro, obter esta informação olhando para a intersectionRect (se este retângulo não for 0×0, o elemento está cruzando o “quadro de captura”), mas ter isso pré-calculado para nós é bastante conveniente.

isIntersecting pode ser usado para descobrir se o elemento observado está apenas entrando no “quadro de captura” ou já está saindo dele. Para descobrir isso, salve o valor dessa propriedade como um sinalizador global e quando a nova entrada para esse elemento chegar à sua função de retorno de chamada, compare seu novo isIntersecting com esse sinalizador global:

  • Se era false e agora é true , então o elemento está entrando no “frame de captura”;
  • Se for o oposto e for false agora enquanto era true antes, então o elemento está deixando o “quadro de captura”.

isIntersecting é exatamente a propriedade que nos ajuda a resolver o problema que discutimos anteriormente, ou seja, separar entradas para os elementos que realmente interceptam o “quadro de captura” do ruído daqueles que são apenas a inicialização da entrada.

 let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { // we are ENTERING the "capturing frame". Set the flag. isLeaving = true; // Do something with entering entry } else if (isLeaving) { // we are EXITING the "capturing frame" isLeaving = false; // Do something with exiting entry } }); }, config);

NOTA : No Microsoft Edge 15, a propriedade isIntersecting não foi implementada, retornando undefined apesar do suporte total para IntersectionObserver . Isso foi corrigido em julho de 2017 e está disponível desde o Edge 16.

A interface IntersectionObserverEntry fornece mais uma propriedade de conveniência pré-calculada: intersectionRatio . Esse parâmetro pode ser usado para os mesmos propósitos que isIntersecting , mas fornece um controle mais granular por ser um número de ponto flutuante em vez de um valor booleano. O valor intersectionRatio indica quanto da área do elemento observado está cruzando o “quadro de captura” (a razão da área de intersectionRect para a área de boundingClientRect ). Novamente, poderíamos fazer esse cálculo usando as informações desses retângulos, mas é bom que isso seja feito para nós.

Já não parece familiar? Sim, a propriedade <code>intersectionRatio</code> é semelhante à propriedade <code>threshold</code> da configuração do Observer. A diferença é que o último define <em>quando</em> acionar o Observer, o primeiro indica a situação real da interseção (que é ligeiramente diferente do <code>threshold</code> devido à natureza assíncrona do Observer).
Já não parece familiar? Sim, a propriedade de intersectionRatio é semelhante à propriedade de threshold da configuração do Observer. A diferença é que o último define *quando* disparar o Observer, o primeiro indica a situação real da interseção (que é ligeiramente diferente do threshold devido à natureza assíncrona do Observer).

target é mais uma propriedade da interface IntersectionObserverEntry que você pode precisar acessar com bastante frequência. Mas não há absolutamente nenhuma mágica aqui – é apenas o elemento original que foi passado para a função observe() do seu Observer. Assim como event.target com o qual você se acostumou ao trabalhar com eventos.

Para obter a lista completa de propriedades para a interface IntersectionObserverEntry , verifique a especificação.

Aplicações possíveis

Percebo que você provavelmente veio a este artigo exatamente por causa deste capítulo: afinal, quem se importa com mecânica quando temos trechos de código para copiar e colar? Portanto, não vou incomodá-lo com mais discussões agora: estamos entrando na terra do código e dos exemplos. Espero que os comentários incluídos no código tornem as coisas mais claras.

Funcionalidade adiada

Em primeiro lugar, vamos rever um exemplo que revela os princípios básicos subjacentes à ideia de IntersectionObserver . Digamos que você tenha um elemento que precise fazer muitos cálculos quando estiver na tela. Por exemplo, seu anúncio deve registrar uma visualização somente quando ele realmente for exibido a um usuário. Mas agora, vamos imaginar que você tenha um elemento de carrossel reproduzido automaticamente em algum lugar abaixo da primeira tela da sua página.

Carrossel abaixo da primeira tela do seu aplicativo
Quando temos um carrossel ou qualquer outra funcionalidade pesada abaixo da dobra de nosso aplicativo, é um desperdício de recursos começar a inicializá-lo/carregá-lo imediatamente.

A execução de um carrossel, em geral, é uma tarefa pesada. Normalmente, envolve cronômetros JavaScript, cálculos para rolar automaticamente pelos elementos, etc. Todas essas tarefas carregam o encadeamento principal e, quando isso é feito no modo de reprodução automática, é difícil saber quando nosso encadeamento principal recebe esse acerto. Quando estamos falando em priorizar o conteúdo em nossa primeira tela e queremos acertar o First Meaningful Paint e o Time To Interactive o mais rápido possível, o thread principal bloqueado se torna um gargalo para nosso desempenho.

Para corrigir o problema, podemos adiar a reprodução desse carrossel até que ele entre na janela de visualização do navegador. Para este caso, empregaremos nosso conhecimento e exemplo para o parâmetro isIntersecting da interface IntersectionObserverEntry .

 const carousel = document.getElementById('carousel'); let isLeaving = false; let observer = new IntersectionObserver(function(entries) { entries.forEach(entry => { if (entry.isIntersecting) { isLeaving = true; entry.target.startCarousel(); } else if (isLeaving) { isLeaving = false; entry.target.stopCarousel(); } }); } observer.observe(carousel);

Aqui, tocamos o carrossel apenas quando ele entra em nossa viewport. Observe a ausência do objeto de config passado para a inicialização do IntersectionObserver : isso significa que contamos com as opções de configuração padrão. Quando o carrossel sair da nossa janela de visualização, devemos parar de jogá-lo para não gastar recursos nos elementos que não são mais importantes.

Carregamento lento de ativos

Este é, provavelmente, o caso de uso mais óbvio para IntersectionObserver : não queremos gastar recursos para baixar algo que o usuário não precisa agora. Isso trará um grande benefício para seus usuários: os usuários não precisarão fazer download e seus dispositivos móveis não precisarão analisar e compilar muitas informações inúteis que não precisam no momento. Sem surpresa, isso também ajudará o desempenho do seu aplicativo.

Imagens de carregamento lento abaixo da dobra
Ativos de carregamento lento, como imagens localizadas abaixo da primeira tela – a aplicação mais óbvia do IntersectionObserver.

Anteriormente, para adiar o download e o processamento de recursos até o momento em que o usuário os colocasse na tela, estávamos lidando com ouvintes de eventos em eventos como scroll . O problema é óbvio: isso acionou os ouvintes com muita frequência. Então tivemos que pensar em estrangular ou debounce na execução do callback. Mas tudo isso adicionou muita pressão em nosso thread principal, bloqueando-o quando mais precisávamos.

Então, voltando ao IntersectionObserver em um cenário de carregamento lento, no que devemos ficar de olho? Vamos verificar um exemplo simples de imagens de carregamento lento.

Veja o carregamento Pen Lazy no IntersectionObserver por Denys Mishunov (@mishunov) no CodePen.

Veja o carregamento Pen Lazy no IntersectionObserver por Denys Mishunov (@mishunov) no CodePen.

Tente rolar lentamente essa página até a “terceira tela” e observe a janela de monitoramento no canto superior direito: ela informará quantas imagens foram baixadas até agora.

No núcleo da marcação HTML para esta tarefa está uma sequência simples de imagens:

 … <img data-src="https://blah-blah.com/foo.jpg"> …

Como você pode ver, as imagens devem vir sem tags src : uma vez que um navegador veja o atributo src , ele começará a baixar imediatamente aquela imagem que é oposta às nossas intenções. Portanto, não devemos colocar esse atributo em nossas imagens em HTML e, em vez disso, podemos confiar em algum atributo de data- como data-src aqui.

Outra parte desta solução é, obviamente, JavaScript. Vamos nos concentrar nos principais bits aqui:

 const images = document.querySelectorAll('[data-src]'); const config = { … }; let observer = new IntersectionObserver(function (entries, self) { entries.forEach(entry => { if (entry.isIntersecting) { … } }); }, config); images.forEach(image => { observer.observe(image); });

Em termos de estrutura, não há nada de novo aqui: já cobrimos tudo isso antes:

  • Recebemos todas as mensagens com nossos atributos data-src ;
  • Set config : para este cenário você deseja expandir seu “frame de captura” para detectar elementos um pouco abaixo da parte inferior da viewport;
  • Registre o IntersectionObserver com essa configuração;
  • Itere sobre nossas imagens e adicione todas elas para serem observadas por este IntersectionObserver ;

A parte interessante acontece dentro da função de retorno de chamada invocada nas entradas. Há três etapas essenciais envolvidas.

  1. Em primeiro lugar, processamos apenas os itens que realmente cruzam nosso “quadro de captura”. Este trecho já deve ser familiar para você.

     entries.forEach(entry => { if (entry.isIntersecting) { … } });

  2. Então, de alguma forma, processamos a entrada convertendo nossa imagem com data-src em um <img src="…"> real.

     if (entry.isIntersecting) { preloadImage(entry.target); … }
    Isso acionará o navegador para finalmente baixar a imagem. preloadImage() é uma função muito simples que não vale a pena ser mencionada aqui. Basta ler a fonte.

  3. Próxima e última etapa: como o carregamento lento é uma ação única e não precisamos baixar a imagem toda vez que o elemento entrar em nosso “quadro de captura”, devemos unobserve a imagem já processada. Da mesma forma que devemos fazer com element.removeEventListener() para nossos eventos regulares quando eles não forem mais necessários para evitar vazamentos de memória em nosso código.

     if (entry.isIntersecting) { preloadImage(entry.target); // Observer has been passed as self to our callback self.unobserve(entry.target); }

Observação. Ao invés de unobserve(event.target) nós também poderíamos chamar disconnect() : ele desconecta completamente nosso IntersectionObserver e não observa mais imagens. Isso é útil se a única coisa com que você se importa é o primeiro hit do seu Observer. No nosso caso, precisamos que o Observer continue monitorando as imagens, então não devemos desconectar ainda.

Sinta-se à vontade para fazer um fork do exemplo e brincar com diferentes configurações e opções. Há uma coisa interessante a ser mencionada quando você deseja carregar as imagens com preguiça em particular. Você deve sempre ter em mente a caixa gerada pelo elemento observado! Se você verificar o exemplo, notará que o CSS para imagens nas linhas 41–47 contém estilos supostamente redundantes, incl. min-height: 100px . Isso é feito para dar aos espaços reservados da imagem ( <img> sem atributo src ) alguma dimensão vertical. Pelo que?

  • Sem dimensões verticais, todas as tags <img> gerariam uma caixa 0×0;
  • Como a tag <img> gera algum tipo de caixa inline-block por padrão, todas essas caixas 0×0 seriam alinhadas lado a lado na mesma linha;
  • Isso significa que seu IntersectionObserver registraria todas (ou, dependendo da rapidez com que você rola, quase todas) as imagens de uma só vez - provavelmente não é exatamente o que você deseja alcançar.

Destaque da seção atual

IntersectionObserver é muito mais do que apenas carregamento lento, é claro. Aqui está outro exemplo de substituição do evento de scroll por essa tecnologia. Neste temos um cenário bastante comum: na barra de navegação fixa devemos destacar a seção atual com base na posição de rolagem do documento.

Veja a seção atual Pen Highlighting no IntersectionObserver por Denys Mishunov (@mishunov) no CodePen.

Veja a seção atual Pen Highlighting no IntersectionObserver por Denys Mishunov (@mishunov) no CodePen.

Estruturalmente, é semelhante ao exemplo para imagens de carregamento lento e tem a mesma estrutura base com as seguintes exceções:

  • Agora queremos observar não as imagens, mas as seções da página;
  • Obviamente, também temos uma função diferente para processar as entradas em nosso retorno de chamada ( intersectionHandler(entry) ). Mas este não é interessante: tudo o que faz é alternar a classe CSS.

O que é interessante aqui é o objeto de config :

 const config = { rootMargin: '-50px 0px -55% 0px' };

Por que não o valor padrão de 0px para rootMargin , você pergunta? Bem, simplesmente porque destacar a seção atual e carregar uma imagem com preguiça são bem diferentes no que tentamos alcançar. Com o carregamento lento, queremos começar a carregar antes que a imagem entre na exibição. Portanto, para esse fim, estendemos nosso “quadro de captura” em 50px na parte inferior. Pelo contrário, quando queremos destacar a seção atual, temos que ter certeza de que a seção está realmente visível na tela. E não apenas isso: temos que ter certeza de que o usuário está, de fato, lendo ou vai ler exatamente esta seção. Portanto, queremos que uma seção vá um pouco mais da metade da janela de visualização da parte inferior antes que possamos declará-la como a seção ativa. Além disso, queremos levar em conta a altura da barra de navegação e, portanto, removemos a altura da barra do “quadro de captura”.

Capturando o quadro para a seção atual
Queremos que o Observer detecte apenas os elementos que entram no 'quadro de captura' entre 50px da parte superior e 55% da janela de visualização da parte inferior.

Além disso, observe que, no caso de destacar o item de navegação atual, não queremos parar de observar nada. Aqui devemos sempre manter o IntersectionObserver no comando, portanto, você não encontrará disconnect() nem unobserve() aqui.

Resumo

IntersectionObserver é uma tecnologia muito simples. Ele tem um suporte muito bom nos navegadores modernos e se você quiser implementá-lo para navegadores que ainda (ou não) o suportam, é claro, existe um polyfill para isso. Mas, em suma, esta é uma ótima tecnologia que nos permite fazer todo tipo de coisas relacionadas à detecção de elementos em uma viewport enquanto ajuda a obter um aumento de desempenho realmente bom.

Por que o IntersectionObserver é bom para você?

  • IntersectionObserver é uma API assíncrona sem bloqueio!
  • IntersectionObserver substitui nossos ouvintes caros em eventos de scroll ou resize .
  • IntersectionObserver faz todos os cálculos caros como getClientBoundingRect() para você para que você não precise.
  • O IntersectionObserver segue o padrão estrutural de outros Observers, portanto, teoricamente, deve ser fácil de entender se você estiver familiarizado com o funcionamento de outros Observers.

Coisas para manter em mente

Se compararmos as capacidades do IntersectionObserver com o mundo de window.addEventListener('scroll') de onde tudo veio, será difícil ver quaisquer contras neste Observer. Então, vamos apenas anotar algumas coisas para manter em mente:

  • Sim, IntersectionObserver é uma API assíncrona sem bloqueio. Isso é ótimo saber! Mas é ainda mais importante entender que o código que você está executando em seus retornos de chamada não será executado de forma assíncrona por padrão, mesmo que a própria API seja assíncrona. Portanto, ainda há uma chance de eliminar todos os benefícios que você obtém do IntersectionObserver se os cálculos da sua função de retorno de chamada tornarem o encadeamento principal sem resposta. Mas esta é uma história diferente.
  • Se você estiver usando o IntersectionObserver para carregamento lento dos ativos (como imagens, por exemplo), execute .unobserve(asset) depois que o ativo for carregado.
  • IntersectionObserver pode detectar interseções apenas para os elementos que aparecem na estrutura de formatação do documento. Para deixar claro: os elementos observáveis ​​devem gerar uma caixa e de alguma forma afetar o layout. Aqui estão apenas alguns exemplos para você entender melhor:

    • Elementos com display: none está fora de questão;
    • opacity: 0 ou visibility:hidden crie a caixa (mesmo que invisível) para que sejam detectadas;
    • Elementos absolutamente posicionados com width:0px; height:0px width:0px; height:0px estão bem. Though, it has to be noted that absolutely positioned elements fully positioned outside of parent's borders (with negative margins or negative top , left , etc.) and are cut out by parent's overflow: hidden won't be detected: their box is out of scope for the formatting structure.
IntersectionObserver: Now You See Me
IntersectionObserver: Now You See Me

I know it was a long article, but if you're still around, here are some links for you to get an even better understanding and different perspectives on the Intersection Observer API:

  • Intersection Observer API on MDN;
  • IntersectionObserver polyfill;
  • IntersectionObserver polyfill as npm module;
  • Lazy-Loading Images with IntersectionObserver [video] by amazing Paul Lewis;
  • Basic and short (just 01:39), but very informative introduction to IntersectionObserver [video] by Surma.

With this, I would like to make a pause in our discussion to give you an opportunity to play with this technology and realize all of its convenience. So, go play with it. The article is finally over. This time I really mean it.