O Guia Completo para Otimização de Desempenho em React
De memo a componentes de servidor — um guia prático para tornar sua aplicação React mais rápida sem super-engenharia.
A otimização de desempenho em React tem um problema de reputação. Pergunte a dez desenvolvedores como tornar uma aplicação React mais rápida, e oito dirão "useMemo e useCallback em tudo". Os dois restantes dirão "otimização prematura é a raiz de todo mal" e sairão andando. Ambos os grupos estão errados, e ambos os grupos entregam aplicações que são desnecessariamente complexas ou desnecessariamente lentas.
A realidade é mais sutil. O modelo de renderização do React é eficiente para a maioria dos casos pronto para uso, mas existem padrões bem compreendidos onde ele é insuficiente. Este guia cobre todas as técnicas importantes de otimização do React — como funcionam, quando ajudam e, crucialmente, quando atrapalham. O objetivo não é fazê-lo usar memo em todos os lugares. É dar a você um modelo mental para desempenho que torna a escolha certa óbvia em toda situação.
Entendendo quando o React re-renderiza
Antes de otimizar qualquer coisa, você precisa entender o que causa uma re-renderização. O React re-renderiza um componente quando seu estado muda, quando seu pai re-renderiza, ou quando um valor de contexto que ele consome muda. Isso parece simples, mas o efeito em cascata é onde os problemas de desempenho se escondem.
Quando um componente pai re-renderiza, todo filho re-renderiza por padrão — mesmo que as props do filho não tenham mudado. O React faz isso porque não pode saber se o filho depende do estado do pai sem executar a função de renderização do filho. Isso não é um bug; é uma escolha de design que mantém o modelo de reconciliação do React simples e previsível. Mas significa que uma mudança de estado em um componente de alto nível pode disparar re-renderizações em dezenas ou centenas de descendentes.
O insight chave é que re-renderizar não é o mesmo que atualizar o DOM. O React compara a saída do DOM virtual (o JSX) com a saída anterior e apenas confirma diferenças no DOM real. Um componente pode re-renderizar mil vezes sem uma única mutação no DOM. O custo está na execução do JavaScript — criar objetos de DOM virtual, executar hooks e fazer diff de árvores. Para árvores de componentes pequenas, esse custo é insignificante. Para listas grandes, gráficos complexos ou componentes que fazem cálculos caros, ele se acumula rapidamente.
// Every keystroke in this input re-renders the entire tree
function SearchPage() {
const [query, setQuery] = useState("");
return (
<div>
<SearchInput value={query} onChange={setQuery} />
<SearchResults query={query} />
<Sidebar>
<FilterPanel />
<RecentSearches />
</Sidebar>
</div>
);
}
// Without optimization, FilterPanel and RecentSearches
// re-render on every keystroke, even though nothing
// they depend on has changed.O primeiro passo em qualquer investigação de desempenho é identificar se você realmente tem um problema. Otimização prematura adiciona complexidade sem benefício mensurável. Profile primeiro, otimize depois — mas quando você identificou um gargalo, as técnicas neste guia são as ferramentas que você usa.
Memoização: memo, useMemo e useCallback
A memoização é a técnica de otimização do React mais discutida e a mais mal aplicada. A ideia central é simples: se uma função produz a mesma saída dadas as mesmas entradas, armazene o resultado em cache e pule a computação em chamadas subsequentes. O React fornece três ferramentas de memoização, cada uma servindo a um propósito diferente.
React.memo envolve um componente e previne re-renderizações quando suas props não mudaram (usando comparação superficial). É mais eficaz para componentes folha que recebem as mesmas props frequentemente — pense em itens de lista, elementos de gráfico ou qualquer componente puro cuja saída de renderização depende apenas de suas props.
import { memo } from "react";
const ExpenseRow = memo(function ExpenseRow({ label, amount }: {
label: string;
amount: number;
}) {
return (
<tr>
<td>{label}</td>
<td className={amount < 0 ? "text-red-500" : "text-green-500"}>
${amount.toFixed(2)}
</td>
</tr>
);
});
// Now ExpenseRow only re-renders when label or amount changes.
// Without memo, it re-renders every time the parent re-renders.useMemo armazena em cache o resultado de uma computação entre renderizações. É útil quando um componente faz um cálculo caro em toda renderização — filtrar um array grande, formatar dados ou executar uma transformação complexa. Sem useMemo, esse trabalho se repete em toda re-renderização mesmo que as entradas não tenham mudado.
function Dashboard({ transactions, filter }: Props) {
// Without useMemo: this runs on every render
const visibleTransactions = useMemo(
() => transactions
.filter(t => t.date >= filter.start && t.date <= filter.end)
.sort((a, b) => b.amount - a.amount),
[transactions, filter]
);
// Without useMemo, filtering and sorting 10,000 items
// happens on every keystroke in any input on the page.
return <TransactionList items={visibleTransactions} />;
}useCallback é o mesmo que useMemo mas para funções. Retorna uma versão memoizada de uma função de callback que só muda quando suas dependências mudam. Isso importa porque funções definidas no corpo da renderização são recriadas a cada renderização, o que quebra a comparação superficial que memo usa.
Aqui está a regra crítica que a maioria dos guias erra: não envolva tudo em useMemo e useCallback. Cada chamada de memoização tem um custo — armazenar o array de dependências, compará-lo em toda renderização e alocar memória de closure. Aplicar memoização a operações baratas torna sua aplicação mais lenta, não mais rápida. A heurística é simples: só memorize se você mediu um problema, ou se a computação é obviamente cara (transformações de dados complexas, operações recursivas, renderizações de listas grandes).
A memoização mais cara é aquela que não deveria ter sido escrita. Profile primeiro. Envolva depois. Toda abstração tem um custo, e memoização é uma abstração sobre o ciclo de renderização.
Um antipadrão comum é envolver tudo em memo defensivamente, esperando prevenir problemas de desempenho. Não funciona. Adiciona sobrecarga a toda comparação de renderização, incha o bundle com código extra e torna a base de código mais difícil de raciocinar. Adicione memoização como uma otimização direcionada após a confirmação do perfil de que ela ajuda, não como um padrão padrão.
Divisão de código com React.lazy e Suspense
O tamanho do bundle é a dimensão de desempenho mais negligenciada em aplicações React. Desenvolvedores se obcecam com otimização de renderização enquanto enviam 500 KB de JavaScript que todo usuário deve baixar, analisar e executar antes de ver qualquer coisa. A divisão de código resolve isso dividindo seu bundle em pedaços que carregam sob demanda.
React.lazy permite renderizar um componente importado dinamicamente como um componente regular. Combinado com Suspense, você pode mostrar uma UI de fallback enquanto o pedaço carrega. A vantagem de desempenho é dupla: o bundle inicial é menor, então a página carrega mais rápido, e os usuários só pagam pelo código que realmente usam.
import { lazy, Suspense } from "react";
const AnalyticsDashboard = lazy(
() => import("./AnalyticsDashboard")
);
const DataExportPanel = lazy(
() => import("./DataExportPanel")
);
function App() {
const [showAnalytics, setShowAnalytics] = useState(false);
return (
<div>
<button onClick={() => setShowAnalytics(true)}>
View Analytics
</button>
<Suspense fallback={<div>Loading...</div>}>
{showAnalytics && <AnalyticsDashboard />}
</Suspense>
</div>
);
}
// AnalyticsDashboard and its dependencies are only loaded
// when the user clicks the button, not on initial page load.Os melhores candidatos para lazy loading são componentes de nível de rota, bibliotecas de visualização pesadas (gráficos, mapas), editores de texto ricos e qualquer componente que esteja abaixo da dobra ou seja acionado por interação do usuário. Uma boa regra prática: se um componente adiciona mais de 20 KB ao bundle e não está visível na renderização inicial, provavelmente deve ser carregado com lazy.
O comportamento melhorado do Suspense no React 19 torna a divisão de código ainda mais prática. A busca de dados dentro de limites Suspense agora está totalmente integrada ao ciclo de vida de renderização, eliminando o problema de cascata onde você precisava carregar o pedaço primeiro e depois buscar seus dados. Combinado com o novo hook use() para leitura de promises, os limites entre estados de carregamento se tornam mais limpos e mais composíveis.
Scroll virtual e otimização de listas
Renderizar listas grandes é o problema de desempenho mais comum em aplicações React reais. Uma lista de 10.000 itens funciona bem em desenvolvimento e desmorona em produção porque o React deve criar e reconciliar 10.000 nós de DOM virtual em toda renderização. O motor de layout do navegador então deve calcular posições para 10.000 nós do DOM. O resultado é uma UI congelada e um usuário frustrado.
O scroll virtual resolve isso renderizando apenas os itens que estão visíveis no viewport, mais um pequeno buffer acima e abaixo. À medida que o usuário rola, itens fora do viewport são desmontados e novos itens são montados. O DOM permanece pequeno — tipicamente 20-30 nós independentemente do tamanho da lista — e o custo de reconciliação do React permanece constante.
import { useVirtualizer } from "@tanstack/react-virtual";
import { useRef } from "react";
function VirtualList({ items }: { items: Item[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48, // estimated row height
overscan: 5, // render 5 extra items off-screen
});
return (
<div ref={parentRef} style={{ height: "600px", overflow: "auto" }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: "absolute",
top: 0,
transform: `translateY(${virtualItem.start}px)`,
height: virtualItem.size,
width: "100%",
}}
>
<ItemRenderer item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
// Only ~25-30 rows are rendered regardless of list size.
// 100,000 items? Same render cost.Além do scroll virtual, o desempenho da lista depende de chaves estáveis. Usar o índice do array como chave faz com que o React identifique erroneamente quais itens mudaram, levando a desmontagens e remontagens desnecessárias de nós do DOM. Use um identificador único dos seus dados — o ID do item — como chave. Isso permite que o React reuse nós do DOM quando itens são reordenados, inseridos ou removidos, o que é significativamente mais barato que destruí-los e recriá-los.
- Use scroll virtual para qualquer lista com probabilidade de exceder 200-500 itens. O limite depende da complexidade do item, mas 500 linhas de complexidade moderada é onde você começa a notar engasgos.
- Sempre use chaves estáveis e únicas derivadas dos seus dados (IDs dos itens). Chaves de índice são um último recurso para listas estáticas que nunca mudam.
- Mantenha os componentes dos itens da lista enxutos. Cada item deve ser um componente de apresentação simples com chamadas de hook mínimas. Computações caras dentro de itens de lista se multiplicam em cada linha renderizada.
- Considere windowing ou paginação para listas que não precisam mostrar todos os itens de uma vez. Às vezes, a melhor otimização é não renderizar algo.
Perfil, análise de bundle e padrões do React 19+
A ferramenta de desempenho mais importante não é uma biblioteca ou um hook. É o profiler do React DevTools. Antes de otimizar qualquer coisa, grave uma sessão de perfil enquanto reproduz o comportamento lento. O profiler mostra exatamente quais componentes renderizaram, por que renderizaram (mudança de prop, mudança de estado, mudança de contexto, re-renderização do pai) e quanto tempo cada renderização levou. Sem esses dados, você está chutando.
A análise de bundle é a segunda ferramenta essencial. Uma aplicação React rápida que envia 800 KB de JavaScript ainda é lenta porque o navegador deve baixá-lo e interpretá-lo. Ferramentas como vite-bundle-visualizer, statoscope ou source-map-explorer geram uma representação visual do seu bundle, mostrando quais pacotes contribuem com mais bytes. Os resultados são frequentemente surpreendentes — uma única dependência grande (moment.js, uma biblioteca de gráficos, um conjunto de ícones) pode representar metade do seu bundle.
O React 19 introduz padrões que mudam o cenário de otimização de desempenho. Server Components são executados no servidor e enviam apenas o HTML renderizado para o cliente, eliminando o JavaScript do lado do cliente para componentes que não precisam de interatividade. Esta não é uma otimização de nicho — qualquer componente que busca dados e os renderiza sem estado do lado do cliente ou manipuladores de eventos deve ser um Server Component. O JavaScript que teria sido enviado ao cliente é zero.
A distinção entre Server Components e Client Components cria um novo modelo mental para desempenho. Antes de recorrer ao memo, pergunte: este componente precisa ser executado no cliente? Se a resposta for não, você eliminou todo custo de re-renderização, todo custo de hidratação e todo byte de JavaScript que este componente teria contribuído para o bundle. Server Components não são um substituto para as ferramentas de otimização do lado do cliente do React. São uma estratégia de nível mais alto que torna as otimizações do lado do cliente desnecessárias para uma grande parte da sua árvore de componentes.
A otimização de imagens é outra área onde o framework lida com o que os desenvolvedores costumavam fazer manualmente. Componentes de imagem do Next.js servem automaticamente imagens responsivas no tamanho e formato corretos, carregam imagens abaixo da dobra com lazy loading e previnem mudança de layout reservando espaço antes do carregamento da imagem. Se você ainda está usando tags img simples com JPEGs grandes, esta única mudança frequentemente produz uma melhoria de desempenho maior do que qualquer estratégia de memoização.
- Execute o profiler do React DevTools na interação mais lenta da sua aplicação antes de fazer qualquer alteração. Salve a gravação como linha de base.
- Analise seu bundle com vite-bundle-visualizer ou source-map-explorer. Procure dependências grandes que podem ser trocadas por alternativas mais leves.
- Converta componentes de busca de dados para Server Components se você estiver usando React 19 com um framework de servidor. Isso elimina todo o custo do lado do cliente.
- Substitua tags img pelo componente de imagem do framework (next/image, etc.) para imagens responsivas automáticas e lazy loading.
- Use a aba de desempenho no Chrome DevTools para medir atraso de interação, mudanças de layout e tarefas longas — não apenas o tempo de renderização do React.
Juntando tudo
A otimização de desempenho em React segue uma hierarquia clara. No topo está a arquitetura: Server Components que rodam no servidor, divisão de código que mantém os bundles pequenos e scroll virtual que limita o tamanho do DOM. No meio está a memoização direcionada: memo para componentes folha que re-renderizam desnecessariamente, useMemo para cálculos caros e useCallback para referências de callback estáveis. Na base — e é por onde a maioria das pessoas começa — está a micro-otimização: funções inline, referências de objeto e encapsulamento prematuro que adicionam complexidade sem benefício mensurável.
Trabalhe de cima para baixo. Profile primeiro, então verifique sua arquitetura, depois aplique memoização direcionada. Não comece com as micro-otimizações. A diferença entre uma aplicação React bem otimizada e uma mal otimizada quase nunca é se você usou memo nos lugares certos. É se sua arquitetura permite que o React faça seu trabalho eficientemente — árvores de componentes pequenas, bundles pequenos, tamanhos de DOM pequenos e fluxo de dados claro.
As técnicas neste guia são abrangentes, mas o princípio é simples: torne o comportamento padrão do React rápido dando a ele menos trabalho para fazer. Renderize menos componentes. Envie menos JavaScript. Compute menos valores. Quanto menos você pedir ao React para fazer, mais rápido ele será — e menos você precisará pensar sobre desempenho.
