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:
- há 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:
- Viewport: área visível do container.
- Overscan: itens extras renderizados “fora” do viewport para suavizar o scroll.
- Row renderer: função que desenha cada linha, recebendo
index,styleekey. - 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
hasNextPageeisNextPageLoading; - implemente deduplicação por
idpara 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 (
rowHeightnúmero): mais rápido e previsível. - Altura variável: use
CellMeasurereCellMeasurerCache, 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
rowRendererestá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:
threshold10–20 - dados pesados (imagens, agregações):
thresholdmenor + 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
AbortControllerpara 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:
-
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.
-
Validação rigorosa de parâmetros
page,limit,cursor, filtros: sempre valide no servidor.- Imponha teto para
limitpara evitar respostas gigantes e DoS acidental.
-
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.
-
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
overscanRowCountestá 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:
- virtualizar sempre (não renderizar milhares de itens de uma vez),
- preferir altura fixa quando possível,
- 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.