Agora você me vê: como adiar, carregar lentamente e agir com o IntersectionObserver
Publicados: 2022-03-10Era 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.
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.
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.
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.
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.
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.
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
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, aroot
é a janela de visualização do seu navegador, mas pode realmente ser qualquer elemento em seu DOM (depois você define aroot
como algo comodocument.getElementById('your-element')
). Tenha em mente que os elementos que você deseja observar devem “viver” na árvore DOM doroot
neste caso.
-
rootMargin
: Define a margem em torno de seu elementoroot
que estende ou reduz o “quadro de captura” quando as dimensões de suaroot
não fornecem flexibilidade suficiente. As opções para os valores desta configuração são semelhantes às demargin
em CSS, comorootMargin: '50px 20px 10px 40px'
(superior, inferior direito, esquerdo). Os valores podem ser abreviados (comorootMargin: '50px'
) e podem ser expressos empx
ou%
. Por padrão,rootMargin: '0px'
.
-
threshold
: nem sempre é desejável reagir instantaneamente quando um elemento observado cruza uma borda do “frame de captura” (definido como uma combinação deroot
erootMargin
).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 dothreshold
(sei que às vezes pode ser confuso), aqui estão alguns exemplos:-
threshold: 0
: O valor padrãoIntersectionObserver
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 oIntersectionObserver
é 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
.
-
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 deroot
. -
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.
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.
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 eratrue
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.
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.
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.
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.
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.
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) { … } });
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); … }
preloadImage()
é uma função muito simples que não vale a pena ser mencionada aqui. Basta ler a fonte.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 comelement.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 caixainline-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.
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”.
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 descroll
ouresize
. -
IntersectionObserver
faz todos os cálculos caros comogetClientBoundingRect()
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 doIntersectionObserver
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
ouvisibility: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 negativetop
,left
, etc.) and are cut out by parent'soverflow: hidden
won't be detected: their box is out of scope for the formatting structure.
- Elementos com
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.