É comum ver desenvolvedores tentando resolver problemas de performance adicionando useMemo, useCallback e memo em praticamente todos os componentes.
O problema é que, na maioria das aplicações React, esses hooks não são os maiores responsáveis pelos ganhos de performance.
Antes de otimizar componentes, normalmente existem gargalos muito maiores, como bundles excessivos, imagens pesadas, renderizações desnecessárias e requisições repetidas.
Páginas que demoram para carregar, interfaces que travam durante a interação e renderizações desnecessárias podem frustrar os usuários e até reduzir a taxa de conversão do produto.
Além disso, a performance também influencia métricas como os Core Web Vitals, utilizados pelo Google como fator de ranqueamento nos resultados de busca.
Leia mais sobre o Core Web Vitals aqui.
Na prática, otimizar performance é um processo, e não apenas uma coleção de hooks.
Neste artigo veremos as principais técnicas para melhorar a performance de aplicações React, entender quando cada otimização faz sentido e, principalmente, como identificar gargalos antes mesmo de escrever qualquer código.
Por Onde Começar a Otimizar?
Uma das dúvidas mais comuns é saber o que otimizar. Em uma grande/média aplicação, é comum pensar em começar pelo maior componente, maior página ou com mais requisições mas tudo isso reflete apenas a organização, não exatamente os maiores gargalos.
Existem algumas ferramentas que ajudam a identificar aonde estão esses gargalos e o que pode ser melhorado, entre eles:
- Chrome DevTools Performance.
- React DevTools Profiler.
- Lighthouse.
- Web Vitals Extension.
- Bundle Analyzer.
Essas ferramentas nos ajudam a entender quais componentes estão renderizando com maior frequência, quanto tempo cada renderização leva, qual o tempo do LCP da página.
Com essas informações podemos nos dedicar a diminuir o tamanho do bundle ou diminuir o tempo de LCP do Core Web Vitals.
Reduza o Bundle Final
Em uma Single Page Application, todo código JavaScript, CSS e HTML são transformado e adicionados no bundle final, um arquivo .js que contêm toda a lógica da aplicação que precisa ser executada pelo navegador.
Quanto maior o bundle final, mais pesado fica.
Podemos usar ferramentas como o Webpack Bundle Analyzer ou Vite Bundle Analyzer para analisar o tamanho do bundle final, as maiores dependências, assets e componentes.

Em muitos casos, reduzir o tamanho do bundle gera um ganho muito maior do que otimizar renderizações.
Lazy Load Components
Podemos carregar componentes pesados e/ou não-urgentes apenas quando eles forem necessários, como modals, gráficos, mapas, dashboard usando o lazy do React.
import { lazy, Suspense } from "react";
const Modal = lazy(() => import("./Modal"));
function App() {
return (
<Suspense fallback={<p>Carregando...</p>}>
<Modal />
</Suspense>
);
}
Assim, o JavaScript do Modal será carregado apenas quando o usuário clicar no botão que abre esse modal.
Code Splitting
Outro benefício da técnica de Lazy Load é dividir o bundle final em arquivos menores que são carregados durante a execução.
Frameworks e bibliotecas de roteamento como React Router permite carregar cada rota separadamente.
const Settings = lazy(() => import("./pages/Settings"));
Dessa forma, o usuário baixa apenas o código necessário para a página que está acessando.
Remova Dependências Desnecessárias
Muitas aplicações importam bibliotecas enormes mas utilizam apenas uma pequena parte delas.
Por exemplo, em vez de importar toda a biblioteca:
import _ from "lodash"; // 71.5k importado.
// _.debounce() Só debounce é usado
Prefira importar apenas a função que você precisa.
import debounce from "lodash/debounce"; // 3.4k importado.
Ou, quando possível, utilize APIs nativas do JavaScript.
Plugins do VSCode como o Import Cost exibem o tamanho da biblioteca importada.

Evite Renderizações Desnecessárias
Sempre que um estado ou propriedade muda, o React renderiza novamente os componentes afetados. Na maioria das vezes isso é esperado.
O problema acontece quando componentes renderizam repetidamente mesmo sem alterar o resultado visual. Em aplicações grandes, isso pode gerar um custo significativo.
O React possui métodos e hooks que melhoram esse problema na maioria das vezes, como o memo, useMemo e useCallback.
React.memo
O método memo do React impede que um componente renderize novamente caso suas propriedades não tenham sido alteradas.
import { memo } from 'react';
function UserCard(props) {
return <p>{props.name}</p>;
}
export const MemorizedUserCard = memo(UserCard);
Sem o memo, o componente renderiza sempre que o componente pai renderiza. Com ele, o React compara as propriedades e reutiliza o resultado anterior caso nada tenha mudado.
Utilize essa estratégia principalmente em:
- Grandes componentes;
- Que são reutilizados diversas vezes;
- Que recebem propriedades estáveis.
React.useMemo
O hook React.useMemo permite armazenar o retorno uma função que faz um cálculo custoso para evitar que ele seja re-calculado a cada renderização. Ela só será executada novamente se o array de dependencias mudar.
const sortedUsers = useMemo(() => {
return [...users].sort(compareUsers);
}, [users]);
Sem ele, a ordenação seria executada em todas as renderizações.
Entretanto, não utilize React.useMemo para operações simples, pois seu próprio custo pode ser maior que o benefício, como no exemplo a seguir.
const isAdmin = useMemo(() => role === "admin", [role]); // useMemo é desnecessário aqui.
React.useCallback
Funções são recriadas a cada renderização e na maioria dos casos isso não representa um problema. Porém, quando uma função é passada para outros componentes via props, uma nova referência pode provocar renderizações desnecessárias.
const handleClick = useCallback((user) => {
saveUser(user);
}, [saveUser]);
<ChildComponent handleClick={handleClick}>
O useCallback mantém a mesma referência da função enquanto suas dependências não mudarem.
Assim como o useMemo, utilize o useCallback apenas quando houver um ganho real.
React Compiler
Para evitar o uso excessivo dos hooks useMemo, useCallback e memo, as versões mais recentes do React introduziram o React Compiler
Na prática, o compilador consegue identificar e aplicar otimizações automaticamente durante o processo de compilação, reduzindo a necessidade de memoização manual.
Embora ainda esteja sendo adotado pelo ecossistema, ele tende a simplificar bastante a otimização de aplicações React no futuro.
Utilize Keys Estáveis
Ao renderizar listas com map, o React utiliza a propriedade key para identificar cada elemento entre uma renderização e outra. Isso permite que o algoritmo de reconciliação determine quais itens foram adicionados, removidos ou atualizados, evitando recriar toda a lista desnecessariamente.
Um erro comum é utilizar o índice do array como key:
{users.map((user, index) => (
<UserCard key={index} user={user} />
))}
Embora funcione em listas estáticas, essa abordagem pode causar renderizações incorretas quando itens são inseridos, removidos ou reordenados, além de fazer o React reutilizar componentes de forma inadequada.
Sempre que possível, utilize um identificador único e estável, como o ID do objeto:
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
Com uma key estável, o React consegue reutilizar os componentes existentes de forma mais eficiente, reduzindo trabalho durante a reconciliação e preservando corretamente o estado de cada item da lista.
Virtualize Listas Grandes
Renderizar centenas ou milhares de elementos simultaneamente pode degradar significativamente a performance. Uma solução é utilizar virtualização.
Bibliotecas como react-window e tanstack virtual renderizam apenas os elementos visíveis na tela, cerca de 20 ou 30 ao invés de renderizar milhares.
Veja esse exemplo usando a biblioteca TanStack Virtual.
Lista com 10.000 itens
Otimize Imagens
Em muitas aplicações, as imagens representam o maior volume de dados baixados pelo navegador.
Algumas boas práticas incluem:
- Utilizar formatos modernos como WebP ou AVIF.
- Comprimir e redimensionar imagens grandes.
- Servir diferentes tamanhos conforme o dispositivo.
- Utilizar CDN arquivos estáticos.
- Lazy Load as imagens (carrega-las quando estão na viewport)
Exemplo:
<img
src="hero.webp"
loading="lazy"
alt="Imagem principal"
/>
Utilize Cache em Requisições da API
Fazer chamadas repetidas para a API aumenta o tempo de resposta e o consumo de rede e nem sempre são atualizados com frequência.
Bibliotecas como React Query ou SWR oferecem recursos como:
- Cache automático;
- Deduplicação de requisições;
- Atualização em segundo plano;
- Sincronização dos dados.
- Cancelar automaticamente requisições.
Exemplo utilizando React Query:
const { data } = useQuery({
queryKey: ["users"],
queryFn: fetchUsers,
staleTime: 1000 * 60 * 5, // Os dados são considerados novos por 5 minutos (sem busca-los novamente nesse período)
gcTime: 1000 * 60 * 10, // Dados não utilizados ficam na memoria do cache por 10 minutos
});
Cancele Requisições Desatualizadas (Debounce & AbortController)
Uma boa prática é adicionar um debounce em funções que interagem com uma API múltiplas vezes dependendo da ação do usuário.
Um exemplo clássico é uma busca. Podemos esperar alguns milisegundos o usuário terminar de digitar para enviar a requisição pro servidor ao invés de fazer a requisição em cada tecla digitada.
Além de tornar a tela mais responsiva, isso diminui o número de chamadas processadas no lado da API.
Podemos usar o setTimeout com o clearTimeout ou a função debounce do lodash.
Outra boa prática é utilizar o AbortController para cancelar requisições que se tornaram obsoletas. Ao utilizar o AbortController, apenas a requisição mais atual permanece ativa, evitando condições de corrida (race conditions) e atualizações incorretas na interface.
import { useEffect, useRef, useState, startTransition } from "react";
export default function Search() {
const [search, setSearch] = useState("");
const [users, setUsers] = useState([]);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const controller = new AbortController();
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(async () => {
const response = await fetch(
`/api/users?search=${encodeURIComponent(search)}`,
{
signal: controller.signal,
}
);
const data = await response.json();
startTransition(() => {
setUsers(data);
});
}, 500);
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
controller.abort();
};
}, [search]);
return (
<>
<input
type="text"
placeholder="Buscar usuários..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</>
);
}
Marque Atualizações de Estados como Não-Urgentes
No exemplo de busca a cima, atualizar o valor do campo de texto deve ser instantâneo para manter a digitação fluida, mas renderizar uma lista de usuários é uma operação mais custosa que pode esperar.
Nesses casos, o React oferece o hook startTransition, que permite marcar atualizações de estado como não urgentes. Dessa forma, o React prioriza atualizações críticas, como a interação do usuário, e agenda as demais para serem executadas quando houver recursos disponíveis.
O resultado é uma interface mais responsiva, especialmente em aplicações com listas grandes, filtros complexos ou renderizações pesadas.
Leia mais sobre o hook startTransition aqui.
Mantenha os Estados Onde São Utilizados
Outro erro comum é armazenar estados muito acima da árvore de componentes porém só utiliza-los em componentes filhos, através de Prop Drilling.
Imagine a seguinte estrutura:
App
├── Header
├── Sidebar
└── Dashboard
Se um estado em App é atualizado, toda a árvore poderá renderizar novamente.
Então sempre que possível, mantenha o estado no componente mais próximo de quem realmente precisa dele.
Estados Globais
Estados globais criados com o Context, Redux, Zustand são ótimos para armazenar, organizar e consumir esses dados em toda aplicação. Mas os cuidados são os mesmos: apenas importe os estados que são utilizados para evitar renderizações desnecessárias.
Utilize o React Profiler
A extensão do navegador React DevTools possui uma ferramenta chamada Profiler que pode ser usada numa aplição React e permite visualizar:
- Quais componentes renderizaram;
- Quanto tempo levaram;
- Quantas vezes renderizaram;
- O motivo da renderização.

Em vez de otimizar por tentativa e erro, utilize o Profiler para tomar decisões baseadas em dados.
Considere Server-Side Rendering (SSR)
Nem todo problema de performance deve ser resolvido no lado do cliente. Frameworks modermos, como Next.JS, Remix, TanStack Start permitem realizar requisições e renderizar as páginas no lado servidor antes de enviá-la ao navegador.
Isso melhora a métrica do LCP (Largest Contentful Paint), diminui a quantidade de JavaScript executada no lado do navegador, além de carregar a página instantaneamente pro usuário, evitando o uso de loading states e fornecendo uma boa experiência.
Conclusão
O React já oferece uma excelente performance por padrão. No entanto, construir aplicações realmente rápidas exige entender como o processo de renderização funciona e saber identificar onde estão os verdadeiros gargalos.
Antes de adicionar useMemo, useCallback ou memo, procure medir a aplicação utilizando ferramentas como o React Profiler e o Lighthouse. Em muitos casos, reduzir o tamanho do bundle, otimizar imagens, utilizar cache para requisições e evitar renderizações desnecessárias produz resultados muito maiores do que adicionar memoização em todos os componentes.
Melhorar a performance é um processo de melhoria contínua. Novas funcionalidades, dependências e componentes podem introduzir regressões ao longo do tempo, por isso, um bom fluxo de trabalho consiste em:
- Medir.
- Identificar o gargalo.
- Aplicar a otimização adequada.
- Medir novamente.
Esse processo garante que cada otimização realmente produza resultados. No fim, performance não é sobre adicionar mais hooks, mas sobre fazer o navegador executar menos trabalho.