Resumo: reutilize os elementos do DOM e remova aqueles que estão longe da janela de visualização. Use marcadores de posição para considerar dados atrasados. Confira uma demonstração e o código do scroller infinito.
A rolagem infinita aparece em toda a Internet. A lista de artistas do Google Music, a linha do tempo do Facebook e o feed ao vivo do Twitter também são um. Você rola para baixo e, antes de chegar ao fim, um novo conteúdo aparece magicamente, aparentemente do nada. É uma experiência integrada para os usuários, e é fácil perceber o apelo.
No entanto, o desafio técnico por trás de um scroller infinito é mais difícil do que parece. A variedade de problemas que você encontra quando quer fazer a coisa certa™ é vasta. Começa com coisas simples, como os links no rodapé se tornando praticamente inacessíveis porque o conteúdo continua empurrando o rodapé para baixo. Mas os problemas ficam mais difíceis. Como você lida com um evento de redimensionamento quando alguém vira o smartphone da posição vertical para a horizontal ou como evita que o smartphone pare de funcionar quando a lista fica muito longa?
The right thing™
Achamos que isso era motivo suficiente para criar uma implementação de referência que mostre uma maneira de resolver todos esses problemas de forma reutilizável, mantendo os padrões de desempenho.
Vamos usar três técnicas para atingir nossa meta: reciclagem do DOM, lápides e fixação de rolagem.
Nosso caso de demonstração será uma janela de chat parecida com o Hangouts, em que podemos rolar as mensagens. A primeira coisa que precisamos é de uma fonte infinita de mensagens de chat. Tecnicamente, nenhum dos scrollers infinitos por aí é verdadeiramente infinito, mas com a quantidade de dados disponíveis para serem inseridos nesses scrollers, eles podem ser considerados assim. Para simplificar, vamos codificar um conjunto de mensagens de chat e escolher mensagens, autores e anexos de imagens aleatoriamente com um pouco de atraso artificial para se comportar um pouco mais como a rede real.

Reciclagem do DOM
A reciclagem do DOM é uma técnica subutilizada para manter baixa a contagem de nós do DOM. A ideia geral é usar elementos DOM já criados que estão fora da tela em vez de criar novos. É verdade que os nós do DOM são baratos, mas não são sem custo financeiro, já que cada um deles adiciona custo extra em memória, layout, estilo e pintura. Dispositivos de baixo custo ficam muito mais lentos, ou até inutilizáveis, se o site tiver um DOM muito grande para gerenciar. Além disso, lembre-se de que cada novo layout e reaplicação dos estilos (um processo acionado sempre que uma classe é adicionada ou removida de um nó) fica mais caro com um DOM maior. Reciclar os nós do DOM significa manter o número total de nós do DOM consideravelmente menor, o que acelera todos esses processos.
O primeiro obstáculo é a rolagem em si. Como só teremos um pequeno subconjunto de todos os itens disponíveis no DOM a qualquer momento, precisamos encontrar outra maneira de fazer com que a barra de rolagem do navegador reflita adequadamente a quantidade de conteúdo que está teoricamente lá. Vamos usar um elemento sentinela de 1 px por 1 px com uma transformação para forçar o elemento que contém os itens (a pista) a ter a altura desejada. Vamos promover cada elemento na passarela para a própria camada e garantir que a camada da passarela esteja completamente vazia. Sem cor de plano de fundo, nada. Se a camada da pista não estiver vazia, ela não vai ser qualificada para as otimizações do navegador, e vamos precisar armazenar uma textura na placa de vídeo com uma altura de algumas centenas de milhares de pixels. Definitivamente não é viável em um dispositivo móvel.
Sempre que rolarmos a tela, vamos verificar se a janela de visualização se aproximou o suficiente do fim da pista. Nesse caso, vamos estender a pista movendo o elemento sentinela e os itens que saíram da janela de visualização para a parte de baixo da pista, preenchendo-os com novo conteúdo.
O mesmo vale para rolar na outra direção. No entanto, nunca vamos reduzir a extensão na nossa implementação para que a posição da barra de rolagem permaneça consistente.
Tombstones
Como mencionamos antes, tentamos fazer com que nossa fonte de dados se comporte como algo no mundo real. Com latência de rede e tudo mais. Isso significa que, se os usuários usarem a rolagem rápida, eles poderão rolar facilmente além do último elemento para o qual temos dados. Se isso acontecer, vamos colocar um item de lápide (um marcador de posição) que será substituído pelo item com conteúdo real assim que os dados chegarem. Os marcadores também são reciclados e têm um pool separado para elementos DOM reutilizáveis. Precisamos disso para fazer uma transição agradável de um tombstone para o item preenchido com conteúdo, o que, de outra forma, seria muito chocante para o usuário e poderia fazer com que ele perdesse o foco do que estava fazendo.

Um desafio interessante aqui é que os itens reais podem ter uma altura maior do que o item de lápide devido a quantidades diferentes de texto por item ou uma imagem anexada. Para resolver isso, vamos ajustar a posição de rolagem atual sempre que os dados chegarem e um marcador estiver sendo substituído acima da janela de visualização, ancorando a posição de rolagem a um elemento em vez de um valor de pixel. Esse conceito é chamado de fixação de rolagem.
Fixação de rolagem
Nossa ancoragem de rolagem será invocada quando os marcadores de exclusão estiverem sendo substituídos e quando a janela for redimensionada (o que também acontece quando os dispositivos são girados). Precisamos descobrir qual é o elemento mais visível na janela de visualização. Como esse elemento pode estar parcialmente visível, também vamos armazenar o deslocamento da parte de cima do elemento em que a janela de visualização começa.

Se a janela de visualização for redimensionada e a faixa tiver mudanças, poderemos restaurar uma situação que pareça visualmente idêntica para o usuário. Você venceu! No entanto, uma janela redimensionada significa que cada item mudou potencialmente de altura. Então, como saber até onde o conteúdo ancorado deve ser colocado? Não fazemos isso. Para descobrir, teríamos que dispor todos os elementos acima do item ancorado e somar todas as alturas. Isso pode causar uma pausa significativa após um redimensionamento, e não queremos isso. Em vez disso, presumimos que todos os itens acima têm o mesmo tamanho de um marcador de túmulo e ajustamos nossa posição de rolagem de acordo. À medida que os elementos são rolados para a pista, ajustamos nossa posição de rolagem, adiando efetivamente o trabalho de layout para quando ele é realmente necessário.
Layout
Esqueci de um detalhe importante: o layout. Cada reciclagem de um elemento do DOM normalmente reajustaria todo o runway, o que nos deixaria bem abaixo da nossa meta de 60 frames por segundo. Para evitar isso, assumimos o ônus do layout e usamos elementos posicionados de forma absoluta com transformações. Dessa forma, podemos fingir que todos os elementos mais acima na pista ainda estão ocupando espaço quando, na verdade, há apenas espaço vazio. Como estamos fazendo o layout por conta própria, podemos armazenar em cache as posições em que cada item termina e carregar imediatamente o elemento correto do cache quando o usuário rola para trás.
O ideal é que os itens sejam repintados apenas uma vez quando forem anexados ao DOM e não sejam afetados por adições ou remoções de outros itens na pista. Isso é possível, mas apenas com navegadores modernos.
Ajustes de ponta
Recentemente, o Chrome adicionou suporte para o isolamento de CSS, um recurso
que permite aos desenvolvedores informar ao navegador que um elemento é um limite para
layout e trabalho de renderização. Como estamos fazendo o layout por conta própria aqui, é uma aplicação
principal para contenção. Sempre que adicionamos um elemento à passarela, sabemos que os outros itens não precisam ser afetados pelo novo layout. Portanto, cada item precisa
ser contain: layout
. Também não queremos afetar o restante do nosso site, então a própria pista de pouso também deve receber essa diretiva de estilo.
Outra coisa que consideramos é usar
IntersectionObservers
como um mecanismo para detectar quando
o usuário rolou o suficiente para começarmos a reciclar elementos e carregar novos
dados. No entanto, os IntersectionObservers são especificados para ter alta latência (como se
usando requestIdleCallback
), então podemos sentir menos capacidade de resposta com
IntersectionObservers do que sem eles. Mesmo nossa implementação atual usando o evento
scroll
sofre desse problema, já que os eventos de rolagem são enviados com base no
"melhor esforço". Eventualmente, o worklet do compositor do Houdini será a solução de alta fidelidade para esse problema.
Ainda não é perfeito
Nossa implementação atual da reciclagem do DOM não é ideal, já que adiciona todos os elementos que passam pela janela de visualização, em vez de se preocupar apenas com aqueles que estão na tela. Isso significa que, quando você rola a tela muito rápido, o Chrome precisa fazer tanto trabalho de layout e renderização que não consegue acompanhar. Você não vai ver nada além do plano de fundo. Não é o fim do mundo, mas definitivamente algo a melhorar.
Esperamos que você entenda como problemas simples podem se tornar desafiadores quando se quer combinar uma ótima experiência do usuário com padrões de alto desempenho. Com os Progressive Web Apps se tornando experiências essenciais em smartphones, isso vai se tornar mais importante, e os desenvolvedores da Web precisarão continuar investindo no uso de padrões que respeitem as restrições de desempenho.
Todo o código pode ser encontrado no nosso repositório. Fizemos o possível para manter o código reutilizável, mas não vamos publicá-lo como uma biblioteca real no npm ou como um repositório separado. O uso principal é educacional.