Por que a Escolha da Lista Certa Importa Tanto
Se você já trabalhou num app React Native com feeds de dados, catálogos de produtos ou qualquer tela que renderiza centenas de itens, sabe que a performance da lista pode salvar ou destruir a experiência do usuário. Rolagem travada, telas em branco no meio do scroll, memória estourando — quem nunca passou por isso, né?
Pois é. Em 2026, com o Expo SDK 55 e o React Native 0.83 rodando obrigatoriamente na Nova Arquitetura, o cenário mudou bastante. Temos agora três opções sérias pra renderizar listas: o bom e velho FlatList, o FlashList v2 da Shopify (que foi reescrito do zero) e o novato LegendList.
Cada um tem sua filosofia, seus pontos fortes e seus trade-offs. E honestamente, a escolha errada pode custar semanas de refatoração depois.
Neste guia, vou comparar as três bibliotecas de forma prática — com benchmarks, código que funciona de verdade e critérios claros pra você decidir qual usar em cada situação.
Como Cada Biblioteca Funciona por Baixo dos Panos
FlatList: Virtualização Clássica
O FlatList é o componente de lista nativo do React Native. Ele usa virtualização: renderiza só os itens visíveis na tela mais uma janela de buffer ao redor. Quando um item sai da viewport, ele é destruído e um novo componente é criado pro item que entra.
Esse ciclo de criar e destruir é justamente o gargalo. Pra listas com itens complexos — imagens, gradientes, animações — o custo de montagem e desmontagem é alto, causando frame drops visíveis. Especialmente em dispositivos Android de baixo custo (e quem já testou num Moto G sabe do que eu tô falando).
FlashList v2: Reciclagem de Células Sem Dependências Nativas
O FlashList v2 é uma reescrita completa da v1. A grande mudança? Agora é uma solução 100% JavaScript, sem dependências nativas. Ele mantém um pool de componentes já montados e, quando um item sai da tela, reutiliza o mesmo componente com dados novos — a famosa reciclagem de células.
Na prática, o custo de renderização cai drasticamente: em vez de montar um componente do zero, apenas as props mudam. O resultado? Até 10x menos uso de CPU na thread JS comparado ao FlatList. Sim, dez vezes.
LegendList: Reciclagem Opcional com Tamanhos Dinâmicos
O LegendList é a opção mais recente, construída sobre a arquitetura Fabric e com foco em eliminar completamente as blank flashes — aquelas telas brancas irritantes que aparecem durante scroll rápido. Diferente do FlashList, a reciclagem no LegendList é opcional (ativada via prop recycleItems) e ele lida nativamente com itens de tamanhos variáveis sem precisar de estimativas. Bem conveniente.
Configuração e Instalação
Pré-requisito: Nova Arquitetura Obrigatória
A partir do Expo SDK 55 e React Native 0.83, a Nova Arquitetura (Fabric + TurboModules + JSI) é obrigatória. O newArchEnabled foi removido do app.json — não tem mais como voltar atrás. Tanto o FlashList v2 quanto o LegendList exigem isso. O FlatList funciona em ambas, mas se beneficia enormemente do Fabric.
Instalando o FlashList v2
npx expo install @shopify/flash-list
Confirme que a versão instalada é 2.x ou superior:
npx expo config --type introspect | grep flash-list
Instalando o LegendList
npx expo install @legendapp/list
Comparativo Prático com Código
Cenário: Lista de Produtos com Imagem, Título e Preço
Vamos usar o mesmo cenário pras três bibliotecas: uma lista de 1.000 produtos com imagem, título e preço. Isso simula um catálogo real de e-commerce — algo que praticamente todo mundo vai precisar implementar em algum momento.
Implementação com FlatList
import React, { useCallback } from 'react';
import { FlatList, View, Text, Image, StyleSheet } from 'react-native';
interface Produto {
id: string;
titulo: string;
preco: number;
imagemUrl: string;
}
const ItemProduto = React.memo(({ item }: { item: Produto }) => (
<View style={styles.card}>
<Image source={{ uri: item.imagemUrl }} style={styles.imagem} />
<View style={styles.info}>
<Text style={styles.titulo}>{item.titulo}</Text>
<Text style={styles.preco}>R$ {item.preco.toFixed(2)}</Text>
</View>
</View>
));
export function ListaProdutosFlatList({ produtos }: { produtos: Produto[] }) {
const renderItem = useCallback(
({ item }: { item: Produto }) => <ItemProduto item={item} />,
[]
);
const keyExtractor = useCallback((item: Produto) => item.id, []);
const getItemLayout = useCallback(
(_: any, index: number) => ({
length: 100,
offset: 100 * index,
index,
}),
[]
);
return (
<FlatList
data={produtos}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
maxToRenderPerBatch={15}
windowSize={5}
removeClippedSubviews={true}
initialNumToRender={10}
/>
);
}
Perceba a quantidade de otimizações manuais: React.memo, useCallback, getItemLayout, ajuste de maxToRenderPerBatch e windowSize. Sem tudo isso, a rolagem de 1.000 itens vai travar visivelmente. É bastante boilerplate pra algo que deveria "simplesmente funcionar".
Implementação com FlashList v2
import React, { useCallback } from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list';
interface Produto {
id: string;
titulo: string;
preco: number;
imagemUrl: string;
}
const ItemProduto = React.memo(({ item }: { item: Produto }) => (
<View style={styles.card}>
<Image source={{ uri: item.imagemUrl }} style={styles.imagem} />
<View style={styles.info}>
<Text style={styles.titulo}>{item.titulo}</Text>
<Text style={styles.preco}>R$ {item.preco.toFixed(2)}</Text>
</View>
</View>
));
export function ListaProdutosFlashList({ produtos }: { produtos: Produto[] }) {
const renderItem = useCallback(
({ item }: { item: Produto }) => <ItemProduto item={item} />,
[]
);
const keyExtractor = useCallback((item: Produto) => item.id, []);
return (
<FlashList
data={produtos}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
);
}
Olha como ficou mais enxuto. No FlashList v2, não precisamos de getItemLayout — o dimensionamento é automático. Não precisamos ajustar maxToRenderPerBatch ou windowSize. A reciclagem de células cuida de tudo. Menos código, mais performance.
Implementação com LegendList
import React, { useCallback } from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
import { LegendList } from '@legendapp/list';
interface Produto {
id: string;
titulo: string;
preco: number;
imagemUrl: string;
}
const ItemProduto = React.memo(({ item }: { item: Produto }) => (
<View style={styles.card}>
<Image source={{ uri: item.imagemUrl }} style={styles.imagem} />
<View style={styles.info}>
<Text style={styles.titulo}>{item.titulo}</Text>
<Text style={styles.preco}>R$ {item.preco.toFixed(2)}</Text>
</View>
</View>
));
export function ListaProdutosLegendList({ produtos }: { produtos: Produto[] }) {
const renderItem = useCallback(
({ item }: { item: Produto }) => <ItemProduto item={item} />,
[]
);
const keyExtractor = useCallback((item: Produto) => item.id, []);
return (
<LegendList
data={produtos}
renderItem={renderItem}
keyExtractor={keyExtractor}
recycleItems={true}
drawDistance={350}
/>
);
}
O LegendList fica no meio-termo em complexidade. Ele permite controle granular com recycleItems e drawDistance. Uma dica importante: se seus itens têm estado local complexo (inputs, timers), desative recycleItems pra evitar bugs sutis. Já perdi horas debugando isso.
Layout Masonry: Estilo Pinterest no React Native
Precisa de um layout tipo Pinterest com colunas de alturas variáveis? O FlashList v2 é a única opção com suporte nativo integrado. Na v1, existia o componente separado MasonryFlashList. Na v2, ficou muito mais simples — basta adicionar a prop masonry:
import { FlashList } from '@shopify/flash-list';
export function GaleriaMasonry({ fotos }: { fotos: Foto[] }) {
return (
<FlashList
data={fotos}
masonry
numColumns={2}
optimizeItemArrangement
renderItem={({ item }) => (
<View style={{ padding: 4 }}>
<Image
source={{ uri: item.url }}
style={{ width: '100%', height: item.altura }}
resizeMode="cover"
/>
</View>
)}
/>
);
}
A prop optimizeItemArrangement redistribui os itens entre as colunas pra minimizar diferenças de altura, gerando um layout visualmente equilibrado. Pra spans personalizados, dá pra fazer assim:
<FlashList
data={fotos}
masonry
numColumns={3}
overrideItemLayout={(layout, item) => {
layout.span = item.destaque ? 2 : 1;
}}
renderItem={({ item }) => <CartaoFoto foto={item} />}
/>
Benchmarks Reais: O que Esperar na Prática
Agora vamos ao que interessa. Com base em testes feitos em 2026 com a Nova Arquitetura habilitada, usando um Pixel 7a (Android intermediário) e um iPhone 13, estes são os números aproximados pra uma lista de 5.000 itens com imagens:
| Métrica | FlatList (otimizado) | FlashList v2 | LegendList |
|---|---|---|---|
| FPS médio scroll (Android) | 38-45 | 55-60 | 55-60 |
| FPS médio scroll (iOS) | 50-55 | 58-60 | 58-60 |
| Uso de CPU thread JS | 60-90% | 5-15% | 8-20% |
| Blank cells no scroll rápido | Frequente | Raro | Muito raro |
| Uso de memória RAM | ~180 MB | ~95 MB | ~110 MB |
| Tempo até primeiro frame | ~320 ms | ~180 ms | ~200 ms |
Os números falam por si. FlashList v2 e LegendList entregam performance muito parecida na maioria dos cenários. A diferença aparece nos extremos: o LegendList tende a ter menos blank cells em scrolls extremamente rápidos, enquanto o FlashList v2 usa menos memória graças à reciclagem mais agressiva.
Mas repara naquele dado de CPU: 5-15% contra 60-90%. Isso não é uma melhoria incremental — é uma mudança de categoria.
Armadilhas Comuns e Como Evitá-las
1. Não adicione a prop key nos itens do FlashList
Esse é o erro mais comum (e mais doloroso) ao migrar do FlatList. No FlatList, adicionar key nos itens é inofensivo. No FlashList, isso desabilita completamente a reciclagem, eliminando toda a vantagem de performance. Já vi times inteiros migrarem pro FlashList e reclamarem que "não fez diferença" — era isso.
// ❌ ERRADO — desabilita a reciclagem
const renderItem = ({ item }) => (
<View key={item.id}>
<Text>{item.nome}</Text>
</View>
);
// ✅ CORRETO — use keyExtractor no componente FlashList
<FlashList
data={dados}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
2. Memoize as props no FlashList v2
O FlashList v2 é mais sensível a mudanças de referência nas props do que a v1. Se você passar objetos ou funções inline, os itens vão ser re-renderizados sem necessidade:
// ❌ ERRADO — função recriada a cada render
<FlashList renderItem={({ item }) => <Item dados={item} />} />
// ✅ CORRETO — função memoizada
const renderItem = useCallback(
({ item }) => <Item dados={item} />,
[]
);
<FlashList renderItem={renderItem} />
3. Cuidado com estado local ao usar reciclagem no LegendList
Se seus itens de lista mantêm estado local (um TextInput com valor controlado, por exemplo), a reciclagem pode causar bugs onde o estado de um item "vaza" pra outro. É sutil e difícil de debugar:
// ⚠️ Se ItemComInput tem useState interno, desative a reciclagem
<LegendList
data={dados}
renderItem={({ item }) => <ItemComInput item={item} />}
recycleItems={false} // Mais seguro para itens com estado local
/>
4. Nunca use ScrollView com map pra listas grandes
Parece óbvio, mas continua aparecendo em projetos reais com uma frequência que me surpreende:
// ❌ NUNCA faça isso com listas grandes
<ScrollView>
{produtos.map(p => <Produto key={p.id} dados={p} />)}
</ScrollView>
// ✅ Use sempre um componente de lista virtualizada
<FlashList data={produtos} renderItem={...} />
Migração do FlatList pro FlashList v2: Passo a Passo
A boa notícia é que a migração é quase um drop-in replacement. Siga estes passos:
- Instale o pacote:
npx expo install @shopify/flash-list - Troque o import: substitua
FlatListdereact-nativeporFlashListde@shopify/flash-list - Remova props desnecessárias:
getItemLayout,maxToRenderPerBatch,windowSize,removeClippedSubviews— o FlashList gerencia tudo sozinho - Remova
keydos componentes de item: use apenaskeyExtractorno FlashList - Memoize
renderItemekeyExtractor: essencial no v2 - Teste em dispositivo Android de baixo custo: é onde os ganhos ficam mais evidentes
// Antes (FlatList)
import { FlatList } from 'react-native';
<FlatList
data={dados}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
maxToRenderPerBatch={15}
windowSize={5}
removeClippedSubviews
/>
// Depois (FlashList v2)
import { FlashList } from '@shopify/flash-list';
<FlashList
data={dados}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
Quando Usar Cada Biblioteca: Guia de Decisão
Então, qual escolher? Aqui vai um guia direto, sem enrolação:
| Cenário | Recomendação | Por quê? |
|---|---|---|
| Lista simples com menos de 200 itens | FlatList | Zero dependências extras, performance aceitável |
| Lista de 200-500 itens simples | FlatList ou FlashList v2 | FlatList com getItemLayout e memo pode dar conta |
| Lista com 500+ itens | FlashList v2 | Reciclagem de células é essencial nessa escala |
| E-commerce com catálogo grande | FlashList v2 | Testado em produção pela Shopify com milhares de itens |
| Layout Masonry / Pinterest | FlashList v2 | Único com suporte nativo a masonry |
| Chat com atualizações em tempo real | FlashList v2 ou LegendList | maintainVisibleContentPosition no FlashList; atualizações dinâmicas no LegendList |
| Feed com mídia pesada (vídeos, GIFs) | LegendList | Melhor handling de blank cells em conteúdo pesado |
| Múltiplas listas na mesma tela | LegendList | Gerenciamento de memória mais granular |
Dicas Avançadas de Performance pra Qualquer Lista
Use React.memo com Comparação Customizada
Pra itens com objetos complexos nas props, uma comparação customizada evita re-renders mesmo quando a referência do objeto muda. Parece detalhe, mas em listas com milhares de itens faz uma diferença enorme:
const ItemProduto = React.memo(
({ item }: { item: Produto }) => (
<View style={styles.card}>
<Text>{item.titulo}</Text>
<Text>R$ {item.preco.toFixed(2)}</Text>
</View>
),
(prevProps, nextProps) =>
prevProps.item.id === nextProps.item.id &&
prevProps.item.preco === nextProps.item.preco
);
Otimize Imagens com expo-image
O componente Image padrão do React Native não tem cache sofisticado. Use expo-image pra ter cache nativo, carregamento progressivo e gerenciamento eficiente de memória — é uma troca que vale muito a pena:
import { Image } from 'expo-image';
const ItemProduto = React.memo(({ item }: { item: Produto }) => (
<View style={styles.card}>
<Image
source={item.imagemUrl}
style={styles.imagem}
contentFit="cover"
placeholder={{ blurhash: item.blurhash }}
transition={200}
recyclingKey={item.id}
/>
</View>
));
A prop recyclingKey é especialmente importante dentro do FlashList: ela garante que a imagem seja atualizada corretamente quando a célula é reciclada. Sem ela, você pode ver a imagem do item anterior por uma fração de segundo antes de trocar.
Mantenha os Seletores de Estado Enxutos
Se cada item da lista depende de estado global (Zustand, Redux), garanta que o componente só se inscreve no estado que realmente usa. Esse é um dos erros mais silenciosos em termos de performance:
// ❌ Re-renderiza quando QUALQUER parte do estado muda
const estado = useStore();
// ✅ Re-renderiza apenas quando o item específico muda
const itemFavorito = useStore(
(state) => state.favoritos[itemId]
);
Perguntas Frequentes
O FlashList v2 funciona na arquitetura antiga do React Native?
Não. O FlashList v2.x foi projetado exclusivamente pra Nova Arquitetura (Fabric). Se seu projeto ainda usa a arquitetura antiga, vai precisar ficar no FlashList v1.x. Mas a partir do Expo SDK 55 e React Native 0.83, a Nova Arquitetura é obrigatória — então projetos novos não vão ter esse problema.
Posso usar o LegendList com Expo?
Sim! O LegendList é 100% JavaScript e funciona perfeitamente com Expo, tanto no managed quanto no bare workflow. Instale com npx expo install @legendapp/list e use direto. Sem configuração nativa adicional.
Qual a diferença entre virtualização e reciclagem de células?
Na virtualização (FlatList), os componentes fora da viewport são destruídos e novos são criados quando entram. Na reciclagem de células (FlashList, LegendList com recycleItems), os componentes são mantidos em memória e reutilizados com dados novos. Pense assim: é como trocar a etiqueta de uma gaveta em vez de demolir e reconstruir a gaveta inteira.
O FlashList v2 substitui completamente o FlatList?
Pra grande maioria dos casos, sim. A API é praticamente idêntica — a migração é quase mecânica. Mas pra listas muito pequenas (menos de 50 itens), o FlatList é suficiente e evita uma dependência externa desnecessária. Nem tudo precisa de otimização máxima.
Como diagnosticar problemas de performance em listas?
Use o React DevTools Profiler pra identificar componentes que re-renderizam demais. No FlashList, a prop onBlankArea reporta automaticamente quando áreas em branco aparecem durante o scroll — é um indicador direto de gargalos. E por favor, sempre teste em dispositivos Android de baixo custo. Problemas de performance que são invisíveis no simulador ou num iPhone recente ficam muito evidentes num Android intermediário.