Virtualize listas grandes com uma janela de reação

Tabelas e listas muito grandes podem diminuir significativamente a performance do seu site. A virtualização pode ajudar.

react-window é uma biblioteca que permite a renderização eficiente de listas grandes.

Por que isso é útil?

Às vezes, é necessário mostrar uma tabela ou lista grande que contenha muitas linhas. Carregar todos os itens em uma lista como essa pode afetar significativamente a performance.

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 início é um subconjunto muito pequeno da lista inteira, e a "janela" de conteúdo visível se move quando o usuário continua rolando. Isso melhora a renderização e a performance de rolagem da lista.

Janela de conteúdo em uma lista virtualizada
Janela de conteúdo móvel em uma lista virtualizada

Os nós do DOM que saem da "janela" são reciclados ou substituídos imediatamente 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.

react-window

react-window é uma biblioteca pequena de terceiros que facilita a criação de listas virtualizadas no aplicativo. Ela oferece várias APIs de base 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 tamanho igual.

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 index argumento (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 posicionados de forma absoluta com os valores de altura e largura atribuídos inline, e o parâmetro style é responsável por isso.

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 forma 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 função de tamanho do item transmitida para a propriedade itemSize randomiza as alturas das linhas neste exemplo. No entanto, em um aplicativo real, precisa haver uma lógica real que defina os tamanhos de cada item. O ideal é que esses tamanhos sejam calculados com base em dados ou obtidos de uma API.

Grades

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

A janela de conteúdo em movimento em uma grade virtualizada é bidimensional
A janela de conteúdo móvel 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 o fato 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 ver exemplos de grades virtualizadas.

Carregamento lento na rolagem

Muitos sites melhoram a performance 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 além de um determinado limite próximo ao final. Embora isso seja melhor do que carregar todos os itens em uma lista de uma só vez, ainda acaba preenchendo o DOM com milhares de entradas de linha se o usuário tiver rolado além desse número. Isso pode levar a um tamanho de DOM excessivamente grande, o 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 normal e uma 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 lentamente novas entradas à medida que o usuário rola para baixo. Um pacote separado, react-window-infinite-loader, torna isso possível com react-window.

Considere o seguinte trecho de 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 filho ListComponent que contém a lista de carregamento infinito. Isso é importante porque o carregador infinito precisa disparar um callback para carregar mais itens quando o usuário rolar além de um determinado ponto.

Confira como o ListComponent que renderiza a lista pode ser:

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 é encapsulado no 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 que é resolvida para dados adicionais da lista

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

A rolagem para baixo na lista pode parecer a mesma, mas agora uma solicitação é feita para recuperar 10 usuários de uma API de usuário aleatória sempre que você rola perto do final da lista. Tudo isso é feito renderizando apenas uma "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á carregando.

Exemplo:

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

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

Overscanning

Como os itens em uma lista virtualizada só mudam quando o usuário rola, um espaço em branco pode aparecer brevemente quando novas entradas estão prestes a ser mostradas. 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 o overscan de itens com a propriedade overscanCount. Isso permite definir quantos itens fora da "janela" visível serão renderizados em todos os momentos.

<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, o overscanning de mais de uma entrada pode ajudar a evitar um flash perceptível de espaço vazio quando o usuário rola. No entanto, o overscanning de muitas entradas pode afetar negativamente a performance. O objetivo de usar uma lista virtualizada é minimizar o número de entradas para o que o usuário pode ver em um determinado momento. Portanto, tente manter o número de itens overscanned 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 overscanned, respectivamente.

Conclusão

Se você não tiver certeza de 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 FPS no Chrome DevTools pode ser usado para analisar 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 a performance.
  3. Se houver determinados recursos indisponíveis em react-window, considere usar react-virtualized se não for possível adicionar essa funcionalidade por conta própria.
  4. Encapsule sua lista virtualizada com react-window-infinite-loader se precisar carregar itens lentamente à medida que o usuário rola.
  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 o overscan de muitas entradas, porque isso afetará negativamente a performance.