Como Usar Virtualização de Listas em React: react-window e react-virtual em Produção Já leu

O Problema da Renderização em Listas Grandes Quando trabalha com listas contendo milhares de itens em React, a aplicação enfrenta um problema fundamental: o navegador precisa renderizar e manter na memória do DOM todos os elementos, mesmo aqueles que não estão visíveis na tela. Isso causa travamentos, consumo excessivo de memória e degrada drasticamente a experiência do usuário. Uma lista com 10 mil itens significa 10 mil componentes React instanciados, 10 mil elementos DOM e toda a bagagem computacional que isso carrega. A virtualização de listas é a solução elegante para esse problema. A ideia é simples: renderize apenas os itens que estão visíveis na viewport do usuário, mais alguns itens adjacentes como buffer. Quando o usuário faz scroll, remova os itens que saíram da visão e adicione os novos. Dessa forma, o número de elementos DOM permanece constante e pequeno, independentemente do tamanho total da lista. Entendendo react-window O Conceito Core React-window é uma biblioteca que implementa virtualização através

O Problema da Renderização em Listas Grandes

Quando trabalha com listas contendo milhares de itens em React, a aplicação enfrenta um problema fundamental: o navegador precisa renderizar e manter na memória do DOM todos os elementos, mesmo aqueles que não estão visíveis na tela. Isso causa travamentos, consumo excessivo de memória e degrada drasticamente a experiência do usuário. Uma lista com 10 mil itens significa 10 mil componentes React instanciados, 10 mil elementos DOM e toda a bagagem computacional que isso carrega.

A virtualização de listas é a solução elegante para esse problema. A ideia é simples: renderize apenas os itens que estão visíveis na viewport do usuário, mais alguns itens adjacentes como buffer. Quando o usuário faz scroll, remova os itens que saíram da visão e adicione os novos. Dessa forma, o número de elementos DOM permanece constante e pequeno, independentemente do tamanho total da lista.

Entendendo react-window

O Conceito Core

React-window é uma biblioteca que implementa virtualização através de dois componentes principais: FixedSizeList para listas com itens de altura/largura fixa, e VariableSizeList para listas onde os itens têm tamanhos diferentes. A biblioteca é mantida por Brian Vaughn, um dos mantenedores do React, e é considerada a solução mais leve e performática disponível.

O funcionamento interno é baseado em calcular qual intervalo de itens deve estar visível baseado na posição de scroll. React-window monitora eventos de scroll, calcula o índice do primeiro e último item visível, e renderiza apenas aquele intervalo. O container mantém o scroll bar com a altura total correta, criando a ilusão de uma lista infinita.

Implementando FixedSizeList

import { FixedSizeList as List } from 'react-window';

const MyList = () => {
  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }));

  const Row = ({ index, style }) => (
    <div style={style} className="list-item">
      <span>{items[index].name}</span>
      <span>{items[index].id}</span>
    </div>
  );

  return (
    <List
      height={600}
      itemCount={items.length}
      itemSize={35}
      width="100%"
    >
      {Row}
    </List>
  );
};

export default MyList;

Neste exemplo, a propriedade style passada pelo componente Row é crucial: ela contém top e height que posicionam o elemento absolutamente dentro da lista virtual. O itemSize de 35 pixels indica que cada item ocupa exatamente esse espaço, permitindo que react-window calcule precisamente qual intervalo renderizar. Com 10 mil itens, apenas cerca de 20 estarão no DOM em qualquer momento.

Usando VariableSizeList

import { VariableSizeList as List } from 'react-window';

const VariableHeightList = () => {
  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    text: `Item ${i}`,
    height: 35 + (i % 3) * 15 // Alguns itens maiores
  }));

  const itemSizes = new Map(items.map(item => [item.id, item.height]));

  const getItemSize = (index) => itemSizes.get(items[index].id);

  const Row = ({ index, style }) => (
    <div 
      style={{
        ...style,
        backgroundColor: index % 2 === 0 ? '#f5f5f5' : '#fff'
      }}
      className="list-item"
    >
      {items[index].text}
    </div>
  );

  return (
    <List
      height={600}
      itemCount={items.length}
      itemSize={getItemSize}
      width="100%"
    >
      {Row}
    </List>
  );
};

export default VariableHeightList;

Quando itens têm alturas variáveis, você fornece uma função para itemSize que retorna a altura específica de cada item. React-window ainda virtualiza eficientemente, mas precisa fazer cálculos mais complexos. Para listas muito grandes com muita variação de tamanho, considere manter um cache das alturas ou estimar valores.

Explorando react-virtual

Quando Usar react-virtual

React-virtual é a biblioteca mantida por TanStack (criadores do React Query e React Table). Ela oferece uma API mais flexível e integra-se naturalmente com React Query e React Table. Enquanto react-window é uma solução completa e pronta para usar, react-virtual é mais um hook primitivo que você compõe com seus próprios elementos.

A abordagem é diferente: react-virtual fornece um hook useVirtualizer que gerencia a lógica de virtualização, deixando o rendering totalmente a seu cargo. Isso oferece mais controle mas requer mais código sua.

Implementação Básica com useVirtualizer

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

const VirtualList = () => {
  const parentRef = useRef();

  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }));

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 35,
    overscan: 10 // Buffer de 10 itens acima e abaixo
  });

  const virtualItems = virtualizer.getVirtualItems();

  return (
    <div
      ref={parentRef}
      style={{
        height: '600px',
        overflow: 'auto'
      }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative'
        }}
      >
        {virtualItems.map(virtualItem => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`
            }}
          >
            <div className="list-item">
              {items[virtualItem.index].name}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

export default VirtualList;

Aqui vemos a abordagem de react-virtual: você passa um elemento de scroll como referência, define o tamanho estimado dos itens, e o hook fornece a lista de itens virtualizados. Você tem controle total sobre o HTML gerado e estilos aplicados. A propriedade overscan define quantos itens fora da viewport devem ser mantidos renderizados para suavizar o scroll.

Integração com React Query

import { useVirtualizer } from '@tanstack/react-virtual';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useRef } from 'react';

const InfiniteVirtualList = () => {
  const parentRef = useRef();

  const { data, hasNextPage, fetchNextPage, isFetchingNextPage } = 
    useInfiniteQuery({
      queryKey: ['items'],
      queryFn: async ({ pageParam = 0 }) => {
        const res = await fetch(`/api/items?offset=${pageParam}`);
        return res.json();
      },
      getNextPageParam: (lastPage, pages) => 
        lastPage.hasMore ? pages.length * 50 : undefined,
      initialPageParam: 0
    });

  const allItems = data?.pages.flatMap(page => page.items) ?? [];

  const virtualizer = useVirtualizer({
    count: hasNextPage ? allItems.length + 1 : allItems.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50,
    overscan: 10,
    measureElement: typeof window !== 'undefined' 
      ? element => element?.getBoundingClientRect().height
      : undefined
  });

  const virtualItems = virtualizer.getVirtualItems();
  const lastVirtualItem = virtualItems[virtualItems.length - 1];

  // Fetch next page quando chegar perto do fim
  if (
    lastVirtualItem?.index >= allItems.length - 1 &&
    hasNextPage &&
    !isFetchingNextPage
  ) {
    fetchNextPage();
  }

  return (
    <div
      ref={parentRef}
      style={{ height: '600px', overflow: 'auto' }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative'
        }}
      >
        {virtualItems.map(virtualItem => (
          <div
            key={virtualItem.index}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`
            }}
          >
            {virtualItem.index < allItems.length ? (
              <div className="item">{allItems[virtualItem.index].name}</div>
            ) : isFetchingNextPage ? (
              <div className="loading">Carregando...</div>
            ) : null}
          </div>
        ))}
      </div>
    </div>
  );
};

export default InfiniteVirtualList;

Essa integração mostra a força de react-virtual: combinar virtualização com dados que chegam incrementalmente. Você monitora quando o usuário se aproxima do fim da lista e dispara fetchNextPage. A virtualização mantém o performance mesmo com dezenas de milhares de itens acumulados.

Comparação Prática e Escolhas

react-window vs react-virtual

React-window é ideal quando você tem uma lista estática completa e quer a solução mais simples. A API é intuitiva, a documentação é clara, e funciona imediatamente. Use quando construir listas tradicionais, grades virtuais ou tabelas onde os dados estão todos disponíveis de uma vez.

React-virtual é preferível quando você precisa integração com React Query, quando tem dados que chegam incrementalmente, ou quando precisa de máximo controle sobre o rendering. A curva de aprendizado é um pouco maior, mas a flexibilidade compensa em cenários complexos.

Métricas de Performance

import { FixedSizeList as List } from 'react-window';
import { useEffect, useState } from 'react';

const PerformanceDemo = () => {
  const [renderTime, setRenderTime] = useState(0);

  useEffect(() => {
    const start = performance.now();

    return () => {
      const end = performance.now();
      setRenderTime(end - start);
    };
  }, []);

  const items = Array.from({ length: 50000 }, (_, i) => ({
    id: i,
    value: Math.random()
  }));

  const Row = ({ index, style }) => (
    <div style={style} className="perf-item">
      Item {items[index].id}: {items[index].value.toFixed(2)}
    </div>
  );

  return (
    <div>
      <p>Tempo de renderização inicial: {renderTime.toFixed(2)}ms</p>
      <List
        height={600}
        itemCount={items.length}
        itemSize={35}
        width="100%"
      >
        {Row}
      </List>
    </div>
  );
};

export default PerformanceDemo;

Teste esse código com 50 mil itens. Sem virtualização, o navegador congelaria. Com virtualização, a renderização inicial é praticamente instantânea, e o scroll é fluido. React-window típicamente renderiza 15-25 itens para uma viewport de 600px com itens de 35px, independentemente do total de itens. React-virtual tem overhead um pouco maior mas oferece mais flexibilidade.

Conclusão

Virtualização de listas é uma técnica essencial quando você trabalha com grandes volumes de dados em React. A escolha entre react-window e react-virtual depende do seu contexto: use react-window para listas estáticas simples e completas, use react-virtual quando integração com React Query ou máxima flexibilidade forem prioridades. Ambas reduzem significativamente o número de elementos DOM, transformando aplicações que seriam inutilizáveis em experiências fluidas e responsivas. O investimento em aprender essas bibliotecas se compensa rapidamente quando você encontra seus primeiros casos reais.

Referências


Artigos relacionados