search

Como Renderizar Milhares de Itens: React Virtualized e Infinite Scroll Otimizado

Como Renderizar Milhares de Itens: React Virtualized e Infinite Scroll Otimizado

Renderizar milhares (ou dezenas de milhares) de itens em uma lista é um dos caminhos mais rápidos para degradar a performance de uma aplicação React. O problema não é apenas “quantos dados existem”, mas quantos elementos o navegador precisa manter no DOM, medir, recalcular layout e repintar a cada rolagem, filtro ou atualização de estado.

A abordagem mais robusta para esse cenário combina virtualização (renderizar somente o que está visível) com infinite scroll (carregar dados sob demanda). Neste guia, o foco é React Virtualized como ferramenta de virtualização e um passo a passo para construir um infinite scroll realmente otimizado — evitando gargalos comuns, requisições redundantes e problemas de confiabilidade.

Por que listas gigantes ficam lentas no React

Quando você renderiza uma lista “completa”, mesmo que cada item seja simples, o custo se acumula em várias camadas:

  • DOM grande: muitos nós aumentam o custo de memória, estilo e layout.
  • Reconciliation do React: comparações e atualizações de muitos elementos.
  • Layout thrashing: medições frequentes (altura/largura) e repaints durante scroll.
  • Eventos e handlers: mesmo com event delegation, o volume de componentes pesa.
  • Imagens e mídia: carregamento e decodificação aumentam uso de CPU e rede.

O resultado típico é scroll “travado”, input lag e consumo elevado de recursos — especialmente em notebooks modestos e dispositivos móveis.

O que é React Virtualized (e quando usar)

React Virtualized é uma biblioteca consolidada para virtualização de listas e tabelas. Ela renderiza apenas os itens dentro da área visível (e uma pequena margem), reciclando elementos conforme o usuário rola. Isso reduz drasticamente:

  • quantidade de nós no DOM;
  • trabalho de layout e pintura;
  • custo de renderização do React.

Você deve considerar React Virtualized quando:

  • muitos itens (centenas a dezenas de milhares);
  • a lista tem scroll contínuo e precisa se manter responsiva;
  • é necessário lidar com itens de altura variável (com estratégia adequada);
  • há necessidade de tabelas, grids e listas com bom controle de performance.

Observação prática: há alternativas modernas como react-window, mas aqui o objetivo é React Virtualized e seus padrões clássicos (List, InfiniteLoader, AutoSizer, CellMeasurer).

Virtualização na prática: conceitos essenciais

Antes do código, vale dominar 4 conceitos:

  1. Viewport: área visível do container.
  2. Overscan: itens extras renderizados “fora” do viewport para suavizar o scroll.
  3. Row renderer: função que desenha cada linha, recebendo index, style e key.
  4. Size/Measurement: altura fixa (mais simples e rápida) ou dinâmica (exige medição e cache).

Se você consegue padronizar a altura dos itens, faça isso: altura fixa simplifica tudo e melhora performance.

Implementação passo a passo com React Virtualized

Abaixo está um exemplo de arquitetura comum para listas grandes com infinite scroll:

  • AutoSizer: adapta a lista ao tamanho do container.
  • InfiniteLoader: dispara carregamento quando o usuário se aproxima do fim.
  • List: lista virtualizada.
  • Estado com controle de paginação: evita requisições duplicadas.

1) Defina um modelo de dados e estado de paginação

Pontos que importam para performance e estabilidade:

  • guarde os itens em um array único;
  • mantenha hasNextPage e isNextPageLoading;
  • implemente deduplicação por id para evitar itens repetidos.
import React, { useCallback, useMemo, useRef, useState } from "react";
import { AutoSizer, InfiniteLoader, List } from "react-virtualized";

const PAGE_SIZE = 50;

export default function VirtualizedInfiniteList() {
  const [items, setItems] = useState([]);
  const [hasNextPage, setHasNextPage] = useState(true);
  const [isNextPageLoading, setIsNextPageLoading] = useState(false);

  // Controle simples para evitar corrida de requisições
  const nextPageRef = useRef(0);

  const isRowLoaded = useCallback(
    ({ index }) => !!items[index],
    [items]
  );

  const loadNextPage = useCallback(async () => {
    if (isNextPageLoading || !hasNextPage) return;

    setIsNextPageLoading(true);

    const page = nextPageRef.current;
    nextPageRef.current += 1;

    try {
      // Exemplo: endpoint paginado por cursor ou page
      const res = await fetch(`/api/items?page=${page}&limit=${PAGE_SIZE}`);
      if (!res.ok) throw new Error("Falha ao buscar dados");

      const data = await res.json();
      const newItems = Array.isArray(data.items) ? data.items : [];

      setItems((prev) => {
        const seen = new Set(prev.map((x) => x.id));
        const merged = [...prev];
        for (const it of newItems) {
          if (it && !seen.has(it.id)) merged.push(it);
        }
        return merged;
      });

      setHasNextPage(Boolean(data.hasNextPage));
    } finally {
      setIsNextPageLoading(false);
    }
  }, [hasNextPage, isNextPageLoading]);

2) Configure InfiniteLoader + List

O InfiniteLoader precisa saber:

  • total de linhas (rowCount) — inclua uma linha extra “placeholder” quando há próxima página;
  • como verificar se uma linha já está carregada;
  • qual função carrega a próxima página.
  const rowCount = useMemo(() => {
    return hasNextPage ? items.length + 1 : items.length;
  }, [items.length, hasNextPage]);

  const rowRenderer = useCallback(
    ({ index, key, style }) => {
      const item = items[index];

      if (!item) {
        return (
          <div key={key} style={style}>
            Carregando...
          </div>
        );
      }

      return (
        <div key={key} style={style}>
          {item.title}
        </div>
      );
    },
    [items]
  );

  return (
    <div style={{ height: "80vh" }}>
      <InfiniteLoader
        isRowLoaded={isRowLoaded}
        loadMoreRows={loadNextPage}
        rowCount={rowCount}
        threshold={10}
      >
        {({ onRowsRendered, registerChild }) => (
          <AutoSizer>
            {({ width, height }) => (
              <List
                ref={registerChild}
                width={width}
                height={height}
                rowCount={rowCount}
                rowHeight={48}
                onRowsRendered={onRowsRendered}
                rowRenderer={rowRenderer}
                overscanRowCount={8}
              />
            )}
          </AutoSizer>
        )}
      </InfiniteLoader>
    </div>
  );
}

O que esse arranjo resolve

  • O DOM fica pequeno (só viewport + overscan).
  • O carregamento é disparado próximo do fim, sem “pular” frames.
  • A linha “Carregando…” aparece enquanto dados não chegaram.

Otimizando de verdade: pontos que mais impactam

1) Altura fixa vs. altura variável

  • Altura fixa (rowHeight número): mais rápido e previsível.
  • Altura variável: use CellMeasurer e CellMeasurerCache, mas saiba que:
    • medir altura custa tempo;
    • imagens carregando depois podem mudar altura e exigir recomputar.

Se precisar de altura variável, imponha limites: tipografia consistente, truncamento, placeholders para imagem com altura reservada.

2) Evite renderizações desnecessárias por item

  • Mantenha o rowRenderer estável (useCallback).
  • Extraia a linha para um componente memoizado quando o conteúdo for complexo.
  • Evite criar funções inline e objetos novos em cada render (props instáveis).

3) Prefetch e threshold sob controle

O threshold define quantas linhas antes do fim o loader dispara. Valores muito baixos geram “buracos” (loading tardio). Muito altos podem causar prefetch exagerado e desperdício de rede.

Uma regra prática:

  • listas leves: threshold 10–20
  • dados pesados (imagens, agregações): threshold menor + caching melhor

4) Cancelamento e concorrência de requisições

Em infinite scroll, o usuário pode:

  • rolar rápido,
  • disparar várias buscas,
  • mudar filtros no meio do carregamento.

O ideal é:

  • usar AbortController para cancelar fetches antigos quando filtros mudam;
  • usar um identificador de “consulta ativa” para descartar respostas atrasadas.

5) Cache e deduplicação

Sem deduplicação, podem surgir itens repetidos por:

  • paginação baseada em offset com dados mutáveis;
  • reexecução de páginas por falhas;
  • concorrência.

Prefira paginação por cursor no backend quando possível, e deduplicate por id no frontend como proteção adicional.

Infinite scroll e segurança: o que costuma ser ignorado

Mesmo sendo um tema de performance, infinite scroll também toca em confiabilidade e segurança operacional:

  1. Rate limiting e backpressure

    • Sem limites, um cliente pode disparar muitas requisições e sobrecarregar a API.
    • No backend, aplique rate limiting por IP/token e limites por rota.
  2. Validação rigorosa de parâmetros

    • page, limit, cursor, filtros: sempre valide no servidor.
    • Imponha teto para limit para evitar respostas gigantes e DoS acidental.
  3. Exposição de dados e autorização

    • Infinite scroll “varre” grandes volumes. Garanta que cada página respeita autorização.
    • Não confie em filtros do cliente para restringir dados.
  4. Logs e observabilidade

    • Registre latência por endpoint, tamanho médio de payload e erros.
    • Monitore picos de paginação e padrões anômalos (scraping/abuso).

Checklist de diagnóstico (quando ainda está lento)

  • O item da lista está pesado? (imagens, sombras, cálculos)
  • Há re-render global da página a cada scroll?
  • rowHeight é fixo? Se não, a medição está estável?
  • O overscanRowCount está exagerado?
  • O backend está respondendo rápido e com payload pequeno?
  • A lista está dentro de um container com tamanho bem definido (para o AutoSizer)?
  • Há deduplicação e controle de concorrência para requisições?

Conclusão

React Virtualized continua sendo uma solução eficiente para renderizar listas enormes com fluidez, desde que você trate virtualização e infinite scroll como um conjunto: reduzir DOM, controlar overscan, estabilizar renderizações e garantir um carregamento incremental previsível.

O maior ganho geralmente vem de três decisões:

  1. virtualizar sempre (não renderizar milhares de itens de uma vez),
  2. preferir altura fixa quando possível,
  3. blindar o carregamento (concorrência, dedupe, limites e validações).

Com essa base, é possível escalar a experiência para dezenas de milhares de itens sem transformar scroll e interação em um gargalo — e sem criar um endpoint que vire alvo fácil de abuso ou de falhas por excesso de carga.

Compartilhar este artigo: