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!

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

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

Por que isso é útil?

Às vezes, é necessário mostrar uma tabela ou lista grande com muitas linhas. Carregar todos os itens de uma lista assim pode afetar muito 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 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 a tela. Isso melhora a renderização e o desempenho de rolagem da lista.

Janela de conteúdo em uma lista virtualizada
Movimentação da "janela" de conteúdo 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 pequena biblioteca de terceiros que facilita a criação de listas virtualizadas no seu aplicativo. Ele oferece 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 um elemento 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 ao 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.

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 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 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. Em um aplicativo real, no entanto, deve haver uma lógica real definindo os tamanhos de cada item. O ideal é que esses tamanhos sejam calculados com base em dados ou obtidos de uma API.

Grades

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

A janela de conteúdo móvel em uma grade virtualizada é bidimensional
Mover a "janela" de conteúdo 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 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 às respectivas propriedades.

Consulte a documentação para ver 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 DOM à lista conforme o usuário rola a tela além de um determinado limite próximo ao final. Embora isso seja melhor do que carregar todos os itens de 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 disso. Isso pode levar a um tamanho excessivamente grande do DOM, o que começa a afetar o desempenho ao tornar 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 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 entradas mais recentes de forma lenta à 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 principal.

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 de carregamento infinito. Isso é importante porque o carregador infinito precisa acionar 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 está envolvido 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 com dados adicionais para a lista

Uma prop de renderização é usada para retornar uma função que o componente de 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 para baixo na lista pode parecer a mesma coisa, mas agora uma solicitação é feita para recuperar 10 usuários de uma API de usuário aleatório 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 a tela, um espaço em branco pode aparecer brevemente quando novas entradas estão prestes a ser exibidas. Você pode tentar rolar rapidamente qualquer um dos exemplos anteriores neste guia para perceber isso.

Para melhorar a experiência do usuário com listas virtualizadas, o react-window permite fazer overscan 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 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 a tela. No entanto, fazer uma varredura excessiva 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 a qualquer momento. Portanto, tente manter o número de itens verificados em excesso 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 supervarreduras, respectivamente.

Conclusão

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

  1. Meça o desempenho de renderização e rolagem. Este artigo mostra como o medidor de FPS no Chrome DevTools pode ser usado para analisar a eficiência da renderização de itens em uma lista.
  2. Inclua react-window em listas ou grades longas que estejam afetando o desempenho.
  3. Se houver recursos que não são compatíveis com react-window, use react-virtualized se você não puder adicionar essa funcionalidade por conta própria.
  4. Encapsule sua lista virtualizada com react-window-infinite-loader se precisar carregar itens de forma lenta à medida que o usuário rola a tela.
  5. Use a propriedade overscanCount para suas listas e as propriedades overscanColumnsCount e overscanRowsCount para suas grades e evite um flash de conteúdo vazio. Não faça uma varredura excessiva de muitas entradas, porque isso vai afetar negativamente a performance.