Resumo: reutilize os elementos 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.
Rolagens infinitas aparecem por toda a Internet. A lista de artistas do Google Play Música é uma, a linha do tempo do Facebook é outra e o feed ao vivo do Twitter é outra. Você rola a tela para baixo e, antes de chegar ao final, um novo conteúdo aparece magicamente do nada. É uma experiência perfeita para os usuários, e é fácil entender 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 longe. Mas os problemas ficam mais difíceis. Como você lida com um evento de redimensionamento quando alguém gira o smartphone da orientação retrato para paisagem ou como você 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 mostrasse uma maneira de resolver todos esses problemas de forma reutilizável, mantendo os padrões de desempenho.
Vamos usar três técnicas para alcançar nosso objetivo: reciclagem de DOM, túmulos e ancoragem de rolagem.
Nosso caso de demonstração será uma janela de chat semelhante ao Hangouts, em que podemos rolar pelas mensagens. A primeira coisa que precisamos é uma fonte infinita de mensagens de chat. Tecnicamente, nenhum dos roladores infinitos é realmente infinito, mas com a quantidade de dados disponíveis para serem enviados a esses roladores, eles podem ser. Para simplificar, vamos codificar um conjunto de mensagens de chat e escolher mensagem, autor e anexo de imagem ocasional de forma aleatória com um pouco de atraso artificial para se comportar um pouco mais como a rede real.

Reciclagem do DOM
A reciclagem de DOM é uma técnica pouco utilizada para manter a contagem de nós DOM baixa. A ideia geral é usar elementos DOM já criados que estão fora da tela em vez de criar novos. Os nós do DOM são baratos, mas não são sem custo financeiro, já que cada um deles adiciona um custo extra em memória, layout, estilo e pintura. Dispositivos de baixo custo vão ficar visivelmente mais lentos, se não completamente inutilizáveis, se o site tiver um DOM muito grande para gerenciar. Além disso, lembre-se de que cada relayout e nova aplicação dos estilos, um processo que é acionado sempre que uma classe é adicionada ou removida de um nó, fica mais caro com um DOM maior. Reciclar seus nós DOM significa que vamos manter o número total de nós DOM consideravelmente menor, tornando todos esses processos mais rápidos.
O primeiro obstáculo é a rolagem em si. Como vamos ter apenas um pequeno subconjunto de todos os itens disponíveis no DOM a qualquer momento, precisamos encontrar outra maneira de fazer a barra de rolagem do navegador refletir adequadamente a quantidade de conteúdo que teoricamente está lá. Vamos usar um elemento sentinela de 1 x 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 da pista para a própria camada para garantir que a camada da pista esteja completamente vazia. Sem cor de plano de fundo, nada. Se a camada da pista não estiver vazia, ela não estará qualificada para as otimizações do navegador e teremos que 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, vamos verificar se a viewport chegou perto o suficiente do final da pista. Nesse caso, vamos estender a pista movendo o elemento sentinela e movendo os itens que saíram da viewport para a parte inferior da pista e os preencheremos com novo conteúdo.
O mesmo vale para a rolagem na outra direção. No entanto, nunca reduzimos a pista na nossa implementação para que a posição da barra de rolagem permaneça consistente.
Lápides
Como mencionamos anteriormente, 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 podem rolar facilmente para além do último elemento com dados. Se isso acontecer, vamos colocar um item de exclusão, um marcador de posição, que será substituído pelo item com o 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 boa transição de uma mensagem de erro para o item preenchido com conteúdo, o que seria muito incômodo para o usuário e poderia fazer com que ele perdesse o foco.

Um desafio interessante aqui é que os itens reais podem ter uma altura maior do que o item de lápide funerário devido a diferentes quantidades de texto por item ou uma imagem anexada. Para resolver isso, vamos ajustar a posição de rolagem atual sempre que os dados forem recebidos e uma lápide funerária for substituída acima da viewport, fixando a posição de rolagem em um elemento em vez de um valor de pixel. Esse conceito é chamado de ancoragem de rolagem.
Ancoragem de rolagem
A ancoragem de rolagem será invocada quando as lápides estiverem sendo substituídas e quando a janela for redimensionada, o que também acontece quando os dispositivos estão sendo virados. Vamos 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 visualização for redimensionada e a pista tiver mudanças, poderemos restaurar uma situação que pareça visualmente idêntica para o usuário. Win! Exceto que uma janela redimensionada significa que cada item pode ter mudado de altura. Então, como saber até onde o conteúdo ancorado precisa ser colocado? Não fazemos isso. Para descobrir, teríamos que posicionar todos os elementos acima do item ancorado e somar todas as alturas. Isso poderia causar uma pausa significativa após um redimensionamento, e não queremos isso. Em vez disso, presumimos que cada item acima tem o mesmo tamanho que uma lápide e ajustamos a posição de rolagem de acordo. À medida que os elementos são rolados para a pista, ajustamos nossa posição de rolagem, adiando o trabalho de layout para quando ele for realmente necessário.
Layout
Esqueci de um detalhe importante: o layout. Cada reciclagem de um elemento DOM normalmente redimensionaria toda a pista, o que nos levaria bem abaixo do nosso objetivo de 60 frames por segundo. Para evitar isso, estamos assumindo a responsabilidade pelo layout e usando elementos posicionados de forma absoluta com transformações. Dessa forma, podemos fingir que todos os elementos mais acima da pista ainda ocupam espaço, quando na verdade há apenas espaço vazio. Como estamos fazendo o layout, 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 pintados 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 última geração
Recentemente, o Chrome adicionou suporte ao CSS Containment, um recurso
que permite que os desenvolvedores informem ao navegador que um elemento é um limite para
o layout e a pintura. Como estamos fazendo o layout aqui, é um aplicativo
principal para contenção. Sempre que adicionamos um elemento à pista, sabemos
que os outros itens não precisam ser afetados pelo redimensionamento. Portanto, cada item precisa
ser contain: layout
. Também não queremos afetar o restante do site.
Portanto, a passarela também precisa receber essa diretiva de estilo.
Outra coisa que consideramos foi usar
IntersectionObservers
como um mecanismo para detectar quando
o usuário rolou o suficiente para que pudéssemos começar a reciclar elementos e carregar novos
dados. No entanto, os IntersectionObservers são especificados para ter alta latência (como se
usassem requestIdleCallback
). Portanto, podemos sentir menos resposta com
IntersectionObservers do que sem eles. Até mesmo nossa implementação atual que usa o
evento scroll
sofre com esse problema, já que os eventos de rolagem são enviados de
forma aleatória. Eventualmente, o Worklet do compositor do Houdini
seria a solução de alta fidelidade para esse problema.
Ainda não é perfeito
Nossa implementação atual de reciclagem de DOM não é ideal, porque adiciona todos os elementos que passam pelo viewport, em vez de se preocupar apenas com os que estão realmente na tela. Isso significa que, quando você rola muito rápido, você coloca tanto trabalho no layout e na pintura no Chrome que ele não consegue acompanhar. Você não vai mais ver nada além do plano de fundo. Não é o fim do mundo, mas com certeza é algo a melhorar.
Esperamos que você perceba como problemas simples podem se tornar desafiadores quando você quer combinar uma ótima experiência do usuário com padrões de alto desempenho. Com os Progressive Web Apps se tornando as principais experiências em smartphones, isso vai se tornar mais importante, e os desenvolvedores da Web vão precisar continuar investindo em 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 a reutilizável, mas não a publicaremos como uma biblioteca real no npm ou como um repositório separado. O uso principal é educacional.