Virtualize listas grandes com uma janela de reação

Tabelas e listas muito grandes podem prejudicar significativamente o desempenho do seu site. A virtualização pode ajudar.

A react-window é uma biblioteca que permite que listas grandes sejam renderizadas de forma eficiente.

Veja um exemplo de uma lista com 1.000 linhas sendo renderizada com react-window. Tente rolar a página o mais rápido possível.

Por que isso é útil?

Às vezes, você precisa mostrar uma tabela ou lista grande que contém muitas linhas. Carregar todos os itens dessa lista pode afetar significativamente o desempenho.

Virtualização de lista, ou "janelamento", é o conceito de renderizar apenas o que está visível para o usuário. O número de elementos renderizados inicialmente é um subconjunto muito pequeno de toda a lista, e a "janela" do conteúdo visível se move quando o usuário continua rolando. Isso melhora a renderização e o desempenho de rolagem da lista.

Janela de conteúdo em uma lista virtualizada
Movimento da "janela" de conteúdo em uma lista virtualizada

Os nós DOM que saem da "janela" são reciclados ou imediatamente substituídos por elementos mais novos à medida que o usuário rola a lista para baixo. Isso mantém o número de todos os elementos renderizados específicos para o tamanho da janela.

react-window

react-window é uma pequena biblioteca de terceiros que facilita a criação de listas virtualizadas no seu aplicativo. Ele fornece várias APIs básicas que podem ser usadas para diferentes tipos de listas e tabelas.

Quando usar listas de tamanho fixo

Use o componente FixedSizeList se você tiver uma lista longa e unidimensional de itens de tamanhos iguais.

import React from 'react';
import { FixedSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const ListComponent = () => (
  <FixedSizeList
    height={500}
    width={500}
    itemSize={120}
    itemCount={items.length}
  >
    {Row}
  </FixedSizeList>
);

export default ListComponent;
  • O componente FixedSizeList aceita uma propriedade height, width e itemSize para controlar o tamanho dos itens na lista.
  • Uma função que renderiza as linhas é transmitida como um filho para FixedSizeList. Os detalhes sobre o item específico podem ser acessados com o argumento index (items[index]).
  • Um parâmetro style também é transmitido para o método de renderização de linha que precisa ser anexado ao elemento de linha. Os itens da lista são totalmente posicionados com os valores de altura e largura atribuídos inline, e o parâmetro style é responsável por isso.

O exemplo do Glitch mostrado anteriormente neste artigo mostra um exemplo de um componente FixedSizeList.

Quando usar listas de tamanho variável

Use o componente VariableSizeList para renderizar uma lista de itens com tamanhos diferentes. Esse componente funciona da mesma maneira que uma lista de tamanho fixo, mas espera uma função para a propriedade itemSize em vez de um valor específico.

import React from 'react';
import { VariableSizeList } from 'react-window';

const items = [...] // some list of items

const Row = ({ index, style }) => (
  <div style={style}>
     {/* define the row component using items[index] */}
  </div>
);

const getItemSize = index => {
  // return a size for items[index]
}

const ListComponent = () => (
  <VariableSizeList
    height={500}
    width={500}
    itemCount={items.length}
    itemSize={getItemSize}
  >
    {Row}
  </VariableSizeList>
);

export default ListComponent;

A incorporação a seguir mostra um exemplo desse componente.

A função de tamanho do item transmitida à propriedade itemSize randomiza as alturas das linhas neste exemplo. No entanto, em um aplicativo real, deve haver uma lógica real que defina os tamanhos de cada item. O ideal é calcular esses tamanhos com base em dados ou em uma API.

Grades

O react-window também oferece suporte à virtualização de listas ou grades multidimensional. Nesse contexto, a "janela" de conteúdo visível muda conforme o usuário rola horizontalmente e verticalmente.

A janela móvel de conteúdo em uma grade virtualizada é bidimensional
Movimento da "janela" do conteúdo em uma grade virtualizada é bidimensional

Da mesma forma, os componentes FixedSizeGrid e VariableSizeGrid podem ser usados dependendo se o tamanho de itens de lista específicos pode variar.

  • Para FixedSizeGrid, a API é quase a mesma, mas com a diferença de que alturas, larguras e contagens de itens precisam ser representadas para colunas e linhas.
  • Para VariableSizeGrid, as larguras das colunas e as alturas das linhas podem ser alteradas transmitindo funções em vez de valores para as respectivas propriedades.

Consulte a documentação para conferir exemplos de grades virtualizadas.

Carregamento lento ao rolar

Muitos sites melhoram o desempenho esperando para carregar e renderizar itens em uma lista longa até que o usuário role para baixo. Essa técnica, comumente chamada de "carregamento infinito", adiciona novos nós do DOM à lista à medida que o usuário rola a tela, além de um determinado limite próximo do final. Embora isso seja melhor do que carregar todos os itens de uma lista de uma só vez, ele ainda acaba preenchendo o DOM com milhares de entradas de linha se o usuário rolar para além deles. Isso pode levar a um tamanho de DOM excessivamente grande, que começa a afetar a performance, tornando os cálculos de estilo e as mutações do DOM mais lentos.

O diagrama a seguir pode ajudar a resumir isso:

Diferença na rolagem entre uma lista normal e uma virtualizada
Diferença na rolagem entre uma lista regular e uma lista virtualizada

A melhor abordagem para resolver esse problema é continuar usando uma biblioteca como react-window para manter uma pequena "janela" de elementos em uma página, mas também carregar de forma lenta as entradas mais recentes conforme o usuário rola a página para baixo. Um pacote separado, react-window-infinite-loader, torna isso possível com react-window.

Considere o seguinte código, que mostra um exemplo de estado gerenciado em um componente App pai.

import React, { Component } from 'react';

import ListComponent from './ListComponent';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      items: [], // instantiate initial list here
      moreItemsLoading: false,
      hasNextPage: true
    };

    this.loadMore = this.loadMore.bind(this);
  }

  loadMore() {
   // method to fetch newer entries for the list
  }

  render() {
    const { items, moreItemsLoading, hasNextPage } = this.state;

    return (
      <ListComponent
        items={items}
        moreItemsLoading={moreItemsLoading}
        loadMore={this.loadMore}
        hasNextPage={hasNextPage}
      />
    );
  }
}

export default App;

Um método loadMore é transmitido para um ListComponent filho que contém a lista infinita de carregadores. Isso é importante porque o loader infinito precisa disparar um callback para carregar mais itens depois que o usuário rola a tela e passa de um determinado ponto.

Confira como o ListComponent que renderiza a lista pode ficar:

import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";

const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
  const Row = ({ index, style }) => (
     {/* define the row component using items[index] */}
  );

  const itemCount = hasNextPage ? items.length + 1 : items.length;

  return (
    <InfiniteLoader
      isItemLoaded={index => index < items.length}
      itemCount={itemCount}
      loadMoreItems={loadMore}
    >
      {({ onItemsRendered, ref }) => (
        <FixedSizeList
          height={500}
          width={500}
          itemCount={itemCount}
          itemSize={120}
          onItemsRendered={onItemsRendered}
          ref={ref}
        >
          {Row}
        </FixedSizeList>
      )}
  </InfiniteLoader>
  )
};

export default ListComponent;

Aqui, o componente FixedSizeList é unido ao InfiniteLoader. As propriedades atribuídas ao carregador são:

  • isItemLoaded: método que verifica se um determinado item foi carregado
  • itemCount: número de itens na lista (ou esperado)
  • loadMoreItems: callback que retorna uma promessa resolvida para dados adicionais da lista.

Uma propriedade de renderização é usada para retornar uma função que o componente da lista usa para renderizar. Os atributos onItemsRendered e ref precisam ser transmitidos.

Confira a seguir um exemplo de como o carregamento infinito pode funcionar com uma lista virtualizada.

Rolar a lista pode parecer o mesmo, mas agora uma solicitação é feita para recuperar 10 usuários de uma API de usuário aleatório sempre que você rola até o fim da lista. Isso é feito enquanto renderiza apenas uma única "janela" de resultados por vez.

Ao verificar o index de um determinado item, um estado de carregamento diferente pode ser exibido para um item, dependendo se uma solicitação foi feita para entradas mais recentes e se o item ainda está sendo carregado.

Exemplo:

const Row = ({ index, style }) => {
  const itemLoading = index === items.length;

  if (itemLoading) {
      // return loading state
  } else {
      // return item
  }
};

Overscan

Como os itens em uma lista virtualizada só mudam quando o usuário rola a tela, o espaço em branco pode piscar brevemente quando as entradas mais recentes estão prestes a aparecer. Você pode tentar rolar rapidamente qualquer um dos exemplos anteriores neste guia para notar isso.

Para melhorar a experiência do usuário com listas virtualizadas, react-window permite que você faça uma varredura excessiva de itens com a propriedade overscanCount. Isso permite definir quantos itens fora da "janela" visível serão renderizados o tempo todo.

<FixedSizeList
  //...
  overscanCount={4}
>
  {...}
</FixedSizeList>

overscanCount funciona para os componentes FixedSizeList e VariableSizeList e tem um valor padrão de 1. Dependendo do tamanho de uma lista e do tamanho de cada item, a sobreposição de mais de uma entrada pode ajudar a evitar um flash perceptível de espaço vazio quando o usuário rola a tela. No entanto, a verificação excessiva de entradas pode afetar negativamente o desempenho. O objetivo de usar uma lista virtualizada é minimizar o número de entradas que o usuário pode ver em determinado momento. Portanto, tente manter o número de itens com leituras excessivas o mais baixo possível.

Para FixedSizeGrid e VariableSizeGrid, use as propriedades overscanColumnsCount e overscanRowsCount para controlar o número de colunas e linhas a serem analisadas, respectivamente.

Conclusão

Se você não souber por onde começar a virtualizar listas e tabelas no aplicativo, siga estas etapas:

  1. Meça a performance de renderização e rolagem. Este artigo mostra como o medidor de QPS no Chrome DevTools pode ser usado para analisar a eficiência da renderização de itens em uma lista.
  2. Inclua react-window para listas ou grades longas que estejam afetando a performance.
  3. Se houver determinados recursos que não têm suporte no react-window, use react-virtualized se não for possível adicionar essa funcionalidade por conta própria.
  4. Una sua lista virtualizada com react-window-infinite-loader se você precisar fazer o carregamento lento de itens conforme o usuário rola a tela.
  5. Use a propriedade overscanCount para suas listas e as propriedades overscanColumnsCount e overscanRowsCount para suas grades para evitar um flash de conteúdo vazio. Não faça a varredura de muitas entradas, porque isso afeta negativamente a performance.