Observador de interseção e rolagem infinita de forma visual

Rolagem infinita de forma visual

LinkVersão curta

A maioria das implementações de rolagem infinita envolve ouvir o evento de rolagem. Mas isso dispara muitas vezes e você também precisa fazer alguns cálculos para verificar se o usuário está na parte inferior da página.

TypeScript
const onWindowScroll = () => {
  console.log('scroll');
  const reachedBottom = isAtTheBottom();
  if (reachedBottom) getMoreItems();
};

Você pode usar throttle ou debounce para otimizá-lo, mas ainda não é o ideal.

A melhor solução é colocar um elemento na parte inferior da página e usar o IntersectionObserver para ser notificado quando o elemento entrar na visualização.

Se essa versão curta foi o suficiente para você, deixe um "gostei" e tenha um ótimo dia! Bom, você também pode até deixar um tweet, se você quiser que eu tenha um ótimo dia.

Se você continuar por aqui, eu vou te mostrar uma implementação de rolagem infinita usando eventos de rolagem, então vamos otimizar seu desempenho com throttling e debouncing, e no final, vou mostrar como fazer isso ainda melhor usando o IntersectionObserver.

LinkIntrodução

Esta é a implementação usando eventos de rolagem, é assim que funciona:

Temos uma função getItems(amount, pointer) que retorna uma Promise de um Array de itens para mostrar. Podemos especificar a quantidade de itens que queremos obter e também podemos dar um ponteiro, que é apenas um número que indica o último item que recebemos.

TypeScript
export const getItems = async (
  amount: number,
  pointer: number = 0
): Promise<{ items: Array<Item>; pointer: number; hasMore: boolean }> => {
  // Wait a second to simulate delay
  await new Promise((res) => setTimeout(res, 1000));

  // Get items
  const itemIndexes =
  const items = Array.from({ length: amount })
    .map((_, i) => i + pointer)
    .map((index) => ITEMS[index] ?? null)
    .filter((v) => v !== null);

  const lastPointer = items.length + pointer;
  const hasMore = lastPointer < ITEMS.length - 1;

  return { items, pointer: lastPointer, hasMore };
};

Quando recebemos os itens, criamos um card para cada e solicitamos mais itens quando o usuário rola para o final da lista de cards.

Para ver se o usuário está na parte inferior, ouvimos eventos de rolagem no document e então executamos uma função chamada isAtTheBottom() que retorna true se o usuário estiver na parte inferior.

TypeScript
window.addEventListener('scroll', () => {
  const reachedBottom = isAtTheBottom();
  if (reachedBottom) {
    // Get the items
    const items = getItems(...);
    // Render a card for each item
    renderCards(items);
  }
})

isAtTheBottom() é a única função relevante até agora, o resto da implementação é apenas estilo, renderização dos cards e um spinner de carregamento. Mas se você está curioso, vou deixar um link para o repositório nas referências.

TypeScript
export const isAtTheBottom = (margin = 0): boolean => {
  const viewportHeight = window.innerHeight;
  const pageHeight = window.document.body.scrollHeight;
  const pageScrollY = window.scrollY;

  const pixelsToReachBottom = pageHeight - (pageScrollY + viewportHeight);
  return pixelsToReachBottom - margin <= 0;
};

LinkO problema com eventos de rolagem

Como eu disse no começo, eventos de rolagem são acionados demais. E chamamos isAtTheBottom() toda vez que um evento de rolagem é acionado.

Veja, vou rolar até o final da página e registrar cada vez que um evento de rolagem for acionado. Veja quantos são.

Já te dei um spoiler, você sabe que vamos abandonar os eventos de rolagem. Mas antes de fazer isso, vou mostrar duas otimizações que podemos fazer para melhorar drasticamente o desempenho enquanto ouvimos os eventos de rolagem.

LinkThrottling

A primeira é o throttling.

Defina uma duração, digamos 100 milissegundos, por exemplo.

Usar 100 milissegundos de throttling nos eventos de rolagem significa deixar um evento de rolagem passar, em seguida, ignorar os outros eventos de rolagem pelos próximos 100 milissegundos.

Throttling

Para usar throttling, podemos criar uma função de ordem superior.

Resumidamente, caso você não saiba o que é isso, uma função de ordem superior é uma função que recebe uma função e retorna outra função. Pode ser difícil de entender no início, estou até escrevendo um livro sobre programação funcional e este é um dos tópicos que abordarei no livro. Um pequeno jabá: você pode se inscrever em nossa newsletter para ser avisado quando o livro estiver pronto.

TypeScript
// Example of a higher order function
export const logDuration =
  (fn) =>
  (...args) => {
    console.log('start', Date.now());
    fn(...args);
    console.log('end', Date.now());
  };

Voltando ao throttling.

Uma implementação simples de throttling em JavaScript pode ser a seguinte:

TypeScript
const throttle = <F extends (...args: Array<any>) => void>(
  fn: F,
  duration: number
): ((...args: Parameters<F>) => void) => {
  let waiting = false; // Initially, we're not waiting
  return function (...args: Parameters<F>): void {
    // We return a throttled function
    if (waiting === false) {
      // If we're not waiting
      fn.apply(this, args); // Execute users function
      waiting = true; // Prevent future invocations
      setTimeout(() => {
        // After a period of time
        waiting = false; // And allow future invocations
      }, duration);
    }
  };
};

Essa implementação é muito simplificada. Eu não recomendo usá-la. Lodash tem uma ótima implementação de throttling que é fortemente testada e muito mais completa. Use Lodash.

LinkDebouncing

Debouncing

Lodash também tem debounce, que é semelhante ao throttle. Eles são tão semelhantes que se você habilitar algumas opções no debounce do Lodash, você chega em throttle.

Já que conseguimos usar algumas opções para fazê-los se comportar da mesma maneira, as diferenças que estou prestes a apresentar estão considerando debounce sem opções, apenas seu comportamento padrão.

Usar 100 milissegundos de debounce nos eventos de rolagem significa deixar um evento de rolagem passar somente depois de 100 milissegundos se passarem sem eventos de rolagem.

Pense neles como se você estivesse nadando em uma piscina e cada lâmpada é um evento. Se você está usando throttling, você consegue respirar de vez em quando. Mas se está usando debouncing, você só consegue respirar depois de um espaço longo o suficiente sem lâmpadas.

Então qual é o melhor? Throttling ou debouncing? Bem, depende do seu caso de uso. Se você realmente está nadando, eu recomendo o throttling.

Eu não vou me aprofundar nessas duas técnicas, mas vou deixar um link nas referências para o artigo do David Corbacho no CSS Tricks onde ele faz um ótimo trabalho explicando como eles funcionam e seus casos de uso. Boa David.

LinkReinterpretando o Algoritmo

Até agora, ouvimos eventos de rolagem e verificamos se o usuário está na parte inferior da página.

Agora deixa eu te fazer uma pergunta: nós realmente nos importamos com eventos de rolagem? Não.

Nós só nos importamos se o usuário está no final da página ou não. Eventos de rolagem são apenas um aviso de que o usuário mudou sua posição. Poderíamos ignorar completamente os eventos de rolagem e executar nossa verificação a cada segundo, funcionaria tão bem quanto.

Então, em vez de ouvir eventos de rolagem, ou um intervalo, ou qualquer outra coisa. Talvez possamos ser mais diretos. E se pudéssemos ouvir quando o usuário chega ao final da página? Isso é tudo que queremos saber, certo?

E podemos fazer isso usando o IntersectionObserver.

LinkObservador de intersecção

O IntersectionObserver observa interseções entre os elementos e a viewport.

Imagine que você está em uma página. A viewport é a parte da página que está atualmente visível. Conforme você rola para cima, a viewport sobe. Conforme você rola para baixo, a viewport desce.

Agora, suponha que você tenha um elemento abaixo de sua viewport. Conforme você rola para baixo, esse elemento cruzará com a viewport, acionando o IntersectionObserver.

Por padrão, o IntersectionObserver dispara assim que um pixel do seu elemento estiver visível. Você pode alterar esse comportamento com a opção threshold. Você pode dizer para disparar apenas quando 100% do elemento for visível, ou para acionar a cada 10%, ou a cada 25%, você escolhe.

Para nossos propósitos, queremos saber quando o usuário chega ao final da página. Então, vamos adicionar um elemento na parte inferior da página e ouvir as interseções nesse elemento. Assim que 1 pixel desse elemento se tornar visível, solicitamos mais itens.

TypeScript
const intersectionObserver = new IntersectionObserver((entries) => {
  const isIntersecting = entries[0]?.isIntersecting ?? false;
  if (isIntersecting) this._getMoreItems();
});

const bottomElement = document.querySelector(...);
intersectionObserver.observe(bottomElement);

Isso é muito mais declarativo e rápido do que usar eventos de rolagem.

LinkMargem de Intersecção

Há outra coisa que gosto de fazer quando estou implementando rolagem infinita. Ao invés de solicitar mais itens quando o usuário chega ao final da página, eu gosto de pedir mais quando o usuário está perto o suficiente do final da página. Digamos... 100 pixels do final, por exemplo.

Para fazer isso, podemos adicionar uma margem superior de 100 pixels ao nosso IntersectionObserver usando a opção rootMargin.

TypeScript
const intersectionObserver = new IntersectionObserver((entries) => {
  const isIntersecting = entries[0]?.isIntersecting ?? false;
  if (isIntersecting) this._getMoreItems();
}, { rootMargin: "100px 0 0 0" });

const bottomElement = document.querySelector(...);
intersectionObserver.observe(bottomElement);

LinkSuporte de navegador

Com relação ao suporte dos navegadores, o IntersectionObserver está disponível em todos os principais navegadores e temos um polyfill se você deseja oferecer suporte a navegadores mais antigos.

LinkInterseções com outros elementos

Existe mais uma opção para personalizar o IntersectionObserver. Não precisamos usar essa opção para rolagem infinita mas vou explicá-la para você só para completar.

Suponha que você tenha um elemento rolável e você quer ouvir interseções entre esse elemento e um de seus elementos filhos. Entendeu? Nesse caso, você não quer observar interseções entre um elemento e a viewport, você deseja observar interseções entre dois elementos. Você pode fazer isso usando a opção root.

TypeScript
const outerElement = document.querySelector(...);
const intersectionObserver =
  new IntersectionObserver(callback, { root: outerElement });

const innerElement = document.querySelector(...);
intersectionObserver.observe(innerElement);

Uma restrição aqui é que o elemento root precisa ser pai dos elementos que você deseja observar.

Se você não especificar o root (ou configurá-lo como null), a viewport será utilizada.

LinkDiretriz Angular

Se você lê nossos artigos, sabe que usamos muito Angular. Para facilitar a nossa vida, criamos uma diretiva para ouvir eventos de interseção.

Ela está disponível na nossa biblioteca de utilidades para Angular. Basta importar o IntersectionObserverModule de @lucaspaganini/angular-utils e usar a diretiva lpIntersection nos seus templates. Você também pode passar opções de interseção com o parâmetro [lpIntersectionOptions]=“options”

TypeScript
import { IntersectionObserverModule } from '@lucaspaganini/angular-utils'

@NgModule({
  ...
    imports: [ ..., IntersectionObserverModule, ... ],
    ...
})
TypeScript
<div
  (lpIntersection)="onIntersection($event)"
  [lpIntersectionOptions]="options"></div>

LinkConclusão

Vou deixar um link para o repositório nas referências.

Enquanto eu estava escrevendo este artigo, comecei a me perguntar se havia uma maneira de implementar rolagem infinita sem adicionar um elemento ao final da lista. Se você tem uma solução para isso ou qualquer outra coisa para contribuir, por favor, deixe um tweet.

Você também pode nos contratar. Não somos uma agência, somos uma equipe, e sou pessoalmente responsável por todos os projetos.Temos uma quantidade limitada de clientes, porque... Bom, basicamente porque sou humano. Mas atualmente estamos disponíveis para novos projetos. Então, vá para lucaspaganini.com e nos diga: Como podemos te ajudar?

Tenha um ótimo dia e nos vemos em breve.

LinkReferências

  1. Repositório GitHub
  2. Debouncing e Throttling explicado por David Corbacho
  3. Intersection Observer Mozilla docs
  4. Throttle Lodash docs
  5. Intersection Observer Polyfill npm
  6. Biblioteca de utilitários Angular npm

Assine a nossa Newsletter e seja avisado quando eu lançar um curso, postar um vídeo ou escrever um artigo.

Campo obrigatório
Campo obrigatório