Virtualize listas grandes com uma janela de reação

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

react-window é uma biblioteca que permite renderizar listas grandes com eficiência.

Veja um exemplo de lista que contém 1.000 linhas sendo renderizadas com react-window. Tente rolar o mais rápido possível.

Por que isso é útil?

Pode haver momentos em que você precisa exibir uma tabela ou lista grande com muitas linhas. Carregar cada item dessa lista pode afetar significativamente o desempenho.

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

Janela de conteúdo em uma lista virtualizada
Movendo "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 recentes à 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.

janela de reação

react-window é uma pequena biblioteca de terceiros que facilita a criação de listas virtualizadas no 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 do mesmo tamanho.

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 filha 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 linhas que precisa ser anexado ao elemento de linha. Os itens de lista são posicionados com os valores de altura e largura atribuídos inline, e o parâmetro style é responsável por isso.

O exemplo de 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 que têm tamanhos diferentes. Esse componente funciona da mesma maneira que uma lista de tamanhos fixos, 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 para a propriedade itemSize randomiza as alturas das linhas neste exemplo. No entanto, em um aplicativo real, é preciso haver uma lógica real definindo os tamanhos de cada item. O ideal é que esses tamanhos sejam calculados com base em dados ou recebidos de uma API.

Grades

react-window também é compatível com a virtualização de listas multidimensionais, ou grades. Nesse contexto, a "janela" de conteúdo visível muda à medida que o usuário rola a tela horizontalmente e verticalmente.

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

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

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

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

Carregamento lento ao rolar

Muitos sites melhoram o desempenho aguardando o carregamento e a renderização de itens em uma lista longa até que o usuário role para baixo. Essa técnica, geralmente conhecida como "carregamento infinito", adiciona novos nós DOM à lista à medida que o usuário rola a tela além de um determinado limite até o fim. Embora isso seja melhor do que carregar todos os itens em uma lista de uma só vez, isso ainda acaba preenchendo o DOM com milhares de entradas de linha caso o usuário tenha passado dessa quantidade. Isso pode levar a um tamanho excessivo do DOM, o que começa a afetar o desempenho, 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 virtualizada
Diferença na rolagem entre uma lista normal e 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 para fazer o carregamento lento de entradas mais recentes à medida que o usuário rola para baixo. Um pacote separado, react-window-infinite-loader, torna isso possível com react-window.

Considere o trecho de código abaixo, 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 a um ListComponent filho que contém a lista infinita de carregadores. Isso é importante porque o carregador infinito precisa disparar um callback para carregar mais itens depois que o usuário tiver rolado além de um determinado ponto.

Veja 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. Os acessórios atribuídos 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 que é resolvida em dados extras para a lista.

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

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

A rolagem da lista para baixo pode parecer a mesma, mas agora é feita uma solicitação para recuperar 10 usuários de uma API de usuário aleatório sempre que você rola para perto do 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 mostrado 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
  }
};

Overscaning

Como os itens em uma lista virtualizada só mudam quando o usuário rola a tela, o espaço em branco pode piscar brevemente à medida que as entradas mais recentes estão prestes a ser exibidas. Tente rolar rapidamente qualquer um dos exemplos anteriores deste guia para perceber isso.

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

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

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

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

Conclusão

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

  1. Medir o desempenho de renderização e rolagem. Este artigo mostra como o medidor de QPS no Chrome DevTools pode ser usado para explorar a eficiência com que os itens são renderizados em uma lista.
  2. Inclua react-window para listas ou grades longas que estejam afetando o desempenho.
  3. Se houver determinados recursos sem suporte no react-window, use react-virtualized se não for possível adicionar essa funcionalidade por conta própria.
  4. Una a lista virtualizada com react-window-infinite-loader se você precisar carregar itens lentamente 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 a exibição de conteúdo vazio. Não verifique muitas entradas em excesso, porque isso afetará negativamente o desempenho.