ESCRITA / № 006
19 jul 20268 min de leitura

Virtualização, como renderizar somente o que importa

Virtualização de listas é uma técnica para que uma aplicação web consiga renderizar listas extensas.

Virtualização, como renderizar somente o que importa
Início8 min de leitura

Toda aplicação web tem ao menos uma listagem ou tabela. Listagem essa que pode ser de produtos, clientes, postagens em uma rede social, e por aí vai. Dependendo do projeto e da UI/UX, pode ser que você não tenha esse tipo de problema. Geralmente, a virtualização é uma saída para quando a listagem possui as seguintes características:

  • Possui scroll infinito, por exemplo: redes sociais;
  • A lista tem muitos dados, milhares e até milhões;
  • Cada linha da lista ou célula da tabela é complexa, ou seja, tem uma UI que vai além de apenas textos simples;

Se o projeto em que você está trabalhando tem essas características, muito provavelmente você precisará implementar virtualização.

Nem toda lista extensa é um problema

Como foi dito acima, quero deixar claro que nem toda lista extensa é um problema. Na grande maioria dos casos, existem duas formas de listar objetos:

A primeira é a listagem paginada, em que você exibe 20 itens, o usuário clica em "próxima página" ou muda o número da página, e você troca o conteúdo. Nesse caso não deve ter problema de performance, porque a tela terá no máximo 20 itens construídos na DOM. Para esse caso, a própria paginação no frontend já é a solução.

A segunda é a listagem infinita, aquela em que você vai descendo a lista e os itens vão aparecendo sem parar, como no feed de uma rede social. Esse é o cenário que geralmente dá problema. Conforme o usuário rola, você vai acumulando itens na tela: 20, depois 40, depois 200, depois 2000. E todos continuam lá, montados, mesmo os que já saíram de vista lá em cima. Obviamente, tudo depende do quão complexa é a sua lista: renderizar 20000 nomes próprios em um texto simples é diferente de renderizar 20000 cards de produtos, com nome, fotos, cor e preço.

Por que renderizar muitos itens é um problema

Precisamos analisar o que acontece quando você renderiza uma lista de dez mil itens usando o Array.map:

// vamos supor que products.length é 20000
function ProductList(products: Product[]) {
  return (
    <ul>
      {products.map(product => (
        <ProductListItem key={product.id} product={product} />
      ))}
    </ul>
  );
}

O código acima está correto, sintaticamente e semanticamente. O problema está no custo que o navegador terá para renderizar e manter cada ProductListItem.

Cada ProductListItem vira um nó na DOM. Dez mil itens viram, no mínimo, dez mil nós — provavelmente muito mais, porque cada item costuma ter uma estrutura interna com vários elementos dentro. Um produto, por exemplo, terá ao menos <img />, <div />, <p />, etc. Cada elemento html pesa na DOM, consumindo RAM e CPU, seja para manter cada elemento em tela, seja para processar eventos (ex: adicionar produto, remover produto). Resumidamente, quanto mais nós, mais memória, mais tempo de layout, mais trabalho para o navegador "pintar" a tela.

Agora, especificamente para o react, existe um gargalo ainda pior, pois ele precisará reconciliar todos os elementos alterados. Toda vez que algum estado muda, ele compara a árvore de componentes pra decidir o que atualizar, e faz esse trabalho mesmo para os itens que estão fora da tela.

image

A imagem acima mostra esse desperdício. A viewport — a área que o usuário realmente visualiza em tela — comporta poucos itens, mesmo a DOM tendo todos eles.

A ideia por trás da virtualização

A virtualização parte de uma observação simples: o usuário só consegue ver o que cabe na tela.

Se a viewport comporta oito itens por vez, não faz sentido ter dez mil elementos na DOM. Faz sentido ter apenas os oito visíveis, e alguns acima e abaixo para a experiência de scroll não ficar ruim. Com isso, os outros não precisariam existir na DOM. Um ponto importante é: eles ainda existem no seu array, na memória, no estado; só não estão renderizados na DOM.

Explicando de outra forma: em vez de renderizar a lista inteira, você renderiza uma janela de itens. Conforme o usuário faz scroll, essa janela desliza pelos dados. Os itens que saem por cima são desmontados, os que entram por baixo são montados. A todo momento, a DOM tem mais ou menos a mesma quantidade de nós — não importa se a lista tem cem ou cem mil itens.

É claro que existem trade-offs na virtualização. Na prática, você está trocando memória por processamento, pois, quando você tem todos os itens na DOM, você não precisa ficar recalculando os itens durante o scroll, e agora, com a virtualização, você precisa. Então, dependendo de como foi feita a virtualização, a experiência de scroll pode ser prejudicada. Técnicas de memoização são muito importantes nessa etapa.

image

A inteligência do scroll

Para a virtualização funcionar, existem alguns truques. Por exemplo: se você só tem oito itens montados, como é que a barra de rolagem sabe que a lista tem dez mil?

A resposta é que ela não sabe: precisamos dizer para ela que existem dez mil itens. A ideia é criar um contêiner com a altura total que a lista teria se estivesse inteira renderizada, e posicionar os poucos itens visíveis no lugar certo dentro desse espaço. Existem outras abordagens além dessa, mas a mais comum é criar esse contêiner.

Para dar um exemplo: se cada item tem 40 pixels de altura e a lista tem dez mil itens, o contêiner precisa ter 10000 * 40 = 400000 pixels de altura. Na prática, é um contêiner sem nada dentro além dos poucos itens montados. Mas para o navegador e para a barra de scroll, ele tem o tamanho de dez mil itens. Dessa forma, a rolagem se comporta exatamente como se a lista inteira estivesse ali.

image

O desenho acima mostra as duas peças: o contêiner "pai" garante que a barra de rolagem tenha o tamanho certo. O pequeno bloco de itens vai se reposicionando conforme o scroll, sempre ocupando a faixa que o usuário está olhando naquele momento.

Como isso funciona na prática

Juntando todas as peças, a virtualização tem quatro partes principais:

  • Calcular a viewport: quantos itens cabem na área visível na tela do usuário.
  • Conhecer a posição do scroll: onde o usuário está na lista naquele momento.
  • Calcular o intervalo visível: com a posição do scroll e a altura de cada item, dá pra saber quais índices do array deveriam estar na tela (por exemplo, do item 120 ao 128).
  • Renderizar apenas esse intervalo: posicionado na altura correta dentro do contêiner de altura total.

A cada evento de scroll, você recalcula o intervalo visível e troca os itens montados. O estado da lista não é alterado: ele continua com os dez mil itens em memória. O que muda é a fatia que está desenhada na tela. Só que aqui entra um ponto relevante: o estado pode mudar caso você esteja implementando paginação no backend, e isso acaba alterando um pouco a forma como é feita a virtualização, pois a lista cresce dinamicamente. Isso poderia ser chamado de lista virtualizada infinita: conforme eu vou fazendo scroll, a paginação no backend vai me trazendo novos itens, e a lista vai crescendo infinitamente. De qualquer forma, o conceito principal da virtualização continua sendo o mesmo.

É por isso que a virtualização escala tão bem: a quantidade de trabalho de renderização não depende do tamanho da lista, e sim do tamanho da tela. Uma lista de cem itens e uma de um milhão renderizam a mesma quantidade de nós na DOM.

Trade-offs

O caso mais simples de virtualizar é quando todos os itens têm a mesma altura. A matemática para esse caso é simples: a posição de qualquer item é índice * altura. Porém, quando cada item da lista possui uma altura diferente, a virtualização acaba aumentando muito a sua complexidade — um card de produto com descrição longa é mais alto que um sem descrição. Nesse caso, você não sabe de antemão onde cada item começa, e precisa medir os itens conforme eles aparecem, ou estimar as alturas e ir corrigindo conforme eles vão renderizando.

Outro ponto é que qualquer coisa que dependa da lista inteira estar na DOM deixa de funcionar. Por exemplo, o ctrl+f do navegador só vai encontrar o que está construído na DOM; leitores de tela precisam de cuidado extra com acessibilidade, já que a maior parte da lista tecnicamente não existe na página. Recursos como "rolar até um item específico" exigem um cálculo manual da posição, já que o item pode nem estar renderizado no momento.

Todos esses trade-offs já possuem solução em várias bibliotecas de virtualização. Uma recomendação é o react-virtuoso.

Conclusão

Virtualização é, no fundo, uma aplicação de uma ideia bem simples: o usuário só enxerga o que cabe na tela, então só o que cabe na tela precisa existir na DOM.

Com isso, uma lista de dez mil, cem mil ou um milhão de itens custa praticamente o mesmo que uma lista de dez, porque o custo passa a depender do tamanho da viewport, e não da quantidade de dados.

Vale ressaltar que você raramente vai implementar isso do zero no dia a dia. Existem bibliotecas maduras que resolvem os casos difíceis (alturas variáveis, scroll restoration, acessibilidade) por você, e na maioria dos projetos é isso que você vai usar. Mas entender o conceito por trás delas é o que te permite saber quando aplicar, o que esperar e por que às vezes a rolagem "pisca" ou some por um instante.

Fim · Obrigado pela leitura