Complexidades de um botão de rolagem infinito

Texto longo, leia o resumo: reutilize seus elementos DOM e remova os que estão distantes da janela de visualização. Use marcadores de posição para considerar os dados atrasados. Confira uma demonstração e o código do botão de rolagem infinito.

Os controles de rolagem infinitos aparecem em toda a Internet. A lista de artistas do Google Music é uma, a linha do tempo do Facebook é uma, e o feed ao vivo do Twitter também é. Você rola a tela para baixo e, antes de chegar ao fim, o novo conteúdo aparece parecendo do nada. É uma experiência perfeita para os usuários e é fácil de ver o apelo.

No entanto, o desafio técnico por trás de um botão de rolagem infinito é mais difícil do que parece. A gama de problemas que você encontra quando quer fazer a coisa certaTM é imensa. Tudo começa com coisas simples, como os links no rodapé tornando praticamente inacessíveis porque o conteúdo continua empurrando o rodapé. Mas os problemas ficam mais difíceis. Como é possível lidar com um evento de redimensionamento quando alguém muda o smartphone de retrato para paisagem ou como evitar que ele fique doloroso até uma parada dolorosa quando a lista fica muito longa?

A coisa certaTM

Acreditamos que isso era motivo suficiente para criar uma implementação de referência que mostrasse uma maneira de resolver todos esses problemas de maneira reutilizável, mantendo os padrões de desempenho.

Vamos usar três técnicas para alcançar nossa meta: reciclagem de DOM, tombstones e fixação de rolagem.

Nosso caso de demonstração será uma janela de bate-papo semelhante a um Hangouts, em que podemos rolar as mensagens. A primeira coisa de que precisamos é uma fonte infinita de mensagens de chat. Tecnicamente, nenhum dos controles de rolagem infinitos é realmente infinito, mas com a quantidade de dados disponíveis para ser bombeado por esses roladores, eles também podem ser. Para simplificar, codificaremos um conjunto de mensagens de chat e selecionaremos a mensagem, o autor e, de vez em quando, anexos de imagem aleatórios com um pouco de atraso artificial para se comportar um pouco mais como a rede real.

Captura de tela do app do Chat

Reciclagem de DOM

A reciclagem de DOMs é uma técnica pouco utilizada 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. É claro que os nós do DOM em si são baratos, mas não são sem custo financeiro, porque cada um deles gera custos extras de memória, layout, estilo e pintura. Os dispositivos mais simples ficarão visivelmente mais lentos se não forem totalmente inutilizáveis se o site tiver um DOM muito grande para gerenciar. Além disso, lembre-se de que toda reformulação e reaplicação dos seus estilos (um processo acionado sempre que uma classe é adicionada ou removida de um nó) fica mais cara com um DOM maior. Reciclar os nós do DOM significa que vamos manter o número total de nós do DOM consideravelmente menor, tornando todos esses processos mais rápidos.

O primeiro obstáculo é a própria rolagem. Como teremos apenas um pequeno subconjunto de todos os itens disponíveis no DOM em um determinado momento, precisamos encontrar outra maneira de fazer com que a barra de rolagem do navegador reflita corretamente a quantidade de conteúdo teórica. Vamos usar um elemento de sentinela de 1 x 1 pixel com uma transformação para forçar o elemento que contém os itens (a passarela) a ter a altura pretendida. Vamos promover cada elemento na passarela para a própria camada para garantir que ela fique completamente vazia. Sem cor de fundo, nada. Se a camada da passarela não estiver vazia, ela não estará qualificada para as otimizações do navegador, e vamos precisar armazenar na placa gráfica uma textura que tenha uma altura de algumas centenas de milhares de pixels. Definitivamente não é viável em um dispositivo móvel.

Sempre que rolamos, verificamos se a janela de visualização chegou suficientemente perto do final da passarela. Nesse caso, vamos estender a pista movendo o elemento de sentinela, movendo os itens que deixaram a janela de visualização para a parte de baixo da pista e preenchê-los com novo conteúdo.

Decolagem Sentinel } }

O mesmo vale para rolagem na outra direção. No entanto, nunca reduziremos a pista na nossa implementação para que a posição da barra de rolagem permaneça consistente.

Lápides

Como mencionamos antes, tentamos fazer com que nossa fonte de dados se comporte como algo real. Com latência de rede e muito mais. Isso significa que, se os usuários usarem a rolagem oscilante, poderão rolar facilmente para além do último elemento de que temos dados. Se isso acontecer, vamos colocar um item de Tombstone, um marcador, que será substituído pelo item por conteúdo real assim que os dados chegarem. Elas também são recicladas e têm um pool separado para elementos DOM reutilizáveis. Precisamos disso para fazer uma boa transição de uma tombstone para o item preenchido com conteúdo, o que, de outra forma, seria muito desagradável para o usuário e poderia fazê-lo perder o foco do foco.

Que tumba. Muito desagradável. Uau.

Um desafio interessante aqui é que itens reais podem ter uma altura maior do que o item de lápide devido a diferentes quantidades de texto por item ou de uma imagem anexada. Para resolver isso, ajustaremos a posição de rolagem atual sempre que os dados chegarem e uma tombstone for substituído acima da janela de visualização, ancorando 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

Nossa ancoragem de rolagem será invocada quando as tombstones estiverem sendo substituídas e quando a janela for redimensionada (o que também acontece quando os dispositivos estão sendo virados). Teremos que descobrir qual é o elemento mais visível na janela de visualização. Como esse elemento só pode ser parcialmente visível, também armazenamos o deslocamento da parte de cima do elemento onde a janela de visualização começa.

Diagrama de ancoragem de rolagem.

Se a janela de visualização for redimensionada e a pista tiver mudanças, poderemos restaurar uma situação visualmente idêntica ao usuário. Conquiste! Exceto uma janela redimensionada significa que a altura de cada item pode ter mudado. Então, como sabemos a que distância o conteúdo ancorado precisa ser colocado? Nós não precisamos! Para descobrir, teríamos que definir o layout de cada elemento acima do item ancorado e adicionar todas as alturas. Isso poderia causar uma pausa significativa após um redimensionamento, e não queríamos isso. Em vez disso, recorremos a presumir que cada item acima tenha o mesmo tamanho de uma tombstone e ajustamos nossa posição de rolagem de acordo. À medida que os elementos são rolados na pista, ajustamos nossa posição de rolagem, adiando efetivamente o trabalho de layout para quando ele for realmente necessário.

Layout

Pulei um detalhe importante: layout. Normalmente, cada reciclagem de um elemento DOM reformularia toda a pista, o que nos deixaria bem abaixo da nossa meta de 60 quadros por segundo. Para evitar isso, assumimos o fardo do layout e usamos elementos absolutamente posicionados com transformações. Dessa forma, podemos fingir que todos os elementos mais acima na passarela ainda estão ocupando espaço quando, na verdade, há apenas espaço vazio. Como já estamos criando 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 só sejam pintados uma vez, quando forem anexados ao DOM e não se incomodarem com a adição ou remoção de outros itens na passarela. Isso é possível, mas apenas com navegadores modernos.

Ajustes de ponta

Recentemente, o Chrome adicionou suporte à contenção de CSS, um recurso que permite aos desenvolvedores informar ao navegador que um elemento é um limite para trabalhos de layout e pintura. Já que estamos fazendo o layout nós mesmos aqui, é uma principal aplicação para contenção. Sempre que adicionamos um elemento à passarela, sabemos que os outros itens não precisam ser afetados pelo relayout. Cada item precisa receber contain: layout. Também não queremos afetar o restante do site, então a própria passarela 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çar a reciclar elementos e carregar novos dados. No entanto, os IntersectionObservers são especificados como tendo alta latência (como se estiver usando requestIdleCallback). Portanto, podemos sentir menos responsivos com IntersectionObservers do que sem. Até mesmo a implementação atual que usa o evento scroll apresenta esse problema, já que os eventos de rolagem são enviados com base no "melhor esforço". Por fim, o Worklet do Compositor da Hudini 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 pela janela de visualização, em vez de apenas se preocupar com aqueles que estão realmente na tela. Isso significa que, quando você rola a tela realmente rápido, trabalha tanto com a pintura e o layout quanto com o Chrome que ele não consegue acompanhar. Você acabará vendo apenas o plano de fundo. Não é o fim do mundo, mas definitivamente algo para melhorar.

Esperamos que você perceba como problemas simples podem se tornar desafiadores quando quiser combinar uma ótima experiência do usuário com altos padrões de desempenho. Com os Progressive Web Apps se tornarem experiências essenciais em smartphones, isso se tornará mais importante, e os desenvolvedores da Web terão que 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 mantê-lo reutilizável, mas não o publicaremos como uma biblioteca real no npm ou como um repositório separado. O uso principal é educacional.