5 Erros de Performance Que Matam Seu Aplicativo React
A maioria dos aplicativos React é mais lenta do que deveria ser. Aqui estão os 5 erros de performance mais comuns que vejo em bases de código em produção — e como corrigir cada um deles.

Analisei centenas de bases de código React ao longo dos anos. Projetos de clientes, repositórios de código aberto, ferramentas internas, MVPs de startups. Os mesmos problemas de performance aparecem repetidamente. Não são casos extremos exóticos — são erros básicos e evitáveis que se acumulam até que o aplicativo fique lento e os usuários comecem a sair.
Aqui estão os cinco que vejo com mais frequência, com soluções reais que você pode aplicar hoje.
1. Re-renderizações Desnecessárias por Falta de Memoização
Este é o problema de performance mais comum em aplicativos React. Um componente pai re-renderiza, e cada filho re-renderiza com ele — mesmo que suas props não tenham mudado. Multiplique isso em uma árvore de componentes profunda com dezenas de itens em uma lista, e você terá um aplicativo que engasga a cada digitação.
O problema:
function Dashboard({ user }) {
const [searchQuery, setSearchQuery] = useState("");
const stats = calculateStats(user.transactions);
return (
<div>
<SearchBar value={searchQuery} onChange={setSearchQuery} />
<StatsPanel stats={stats} />
<TransactionList transactions={user.transactions} />
</div>
);
}
Toda vez que searchQuery muda, calculateStats é executado novamente — mesmo que user.transactions não tenha mudado. StatsPanel e TransactionList também re-renderizam sem motivo.
A solução:
function Dashboard({ user }) {
const [searchQuery, setSearchQuery] = useState("");
const stats = useMemo(
() => calculateStats(user.transactions),
[user.transactions]
);
const handleSearchChange = useCallback(
(value: string) => setSearchQuery(value),
[]
);
return (
<div>
<SearchBar value={searchQuery} onChange={handleSearchChange} />
<MemoizedStatsPanel stats={stats} />
<MemoizedTransactionList transactions={user.transactions} />
</div>
);
}
const MemoizedStatsPanel = memo(StatsPanel);
const MemoizedTransactionList = memo(TransactionList);
Três coisas aconteceram aqui:
useMemoimpede quecalculateStatsseja executado em cada renderização. Ele só recalcula quandouser.transactionsrealmente muda.useCallbackestabiliza a referência ao manipulador de mudança para queSearchBarnão veja uma função "nova" em cada renderização.memoenvolve os componentes filhos para que eles pulem a re-renderização quando suas props forem superficialmente iguais.
Uma palavra de cautela: não envolva todos os componentes em memo cegamente. Primeiro, faça um perfil usando o React DevTools Profiler. A memoização tem um custo — a própria comparação superficial. Aplique-a onde importa: computações caras, listas grandes e componentes que re-renderizam frequentemente com props inalteradas.
2. Bundles Inchados por Falta de Code Splitting
Regularmente vejo aplicativos React que entregam tudo em um único bundle JavaScript. O painel de administração, a página de configurações, o centro de ajuda raramente visitado — tudo carregado antecipadamente na primeira visita à página. O usuário queria ver uma landing page, e seu navegador acabou de baixar 2 MB de JavaScript que ainda não precisava.
O problema:
import AdminDashboard from "./pages/AdminDashboard";
import Settings from "./pages/Settings";
import HelpCenter from "./pages/HelpCenter";
import Landing from "./pages/Landing";
function App() {
return (
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/help" element={<HelpCenter />} />
</Routes>
);
}
Importações estáticas significam que o bundler inclui tudo em um único chunk. Não importa qual rota o usuário visite, ele baixa tudo.
A solução:
import { lazy, Suspense } from "react";
const AdminDashboard = lazy(() => import("./pages/AdminDashboard"));
const Settings = lazy(() => import("./pages/Settings"));
const HelpCenter = lazy(() => import("./pages/HelpCenter"));
import Landing from "./pages/Landing"; // Keep critical path static
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/help" element={<HelpCenter />} />
</Routes>
</Suspense>
);
}
React.lazy com import() dinâmico informa ao bundler para dividir essas páginas em chunks separados. Eles são carregados sob demanda quando o usuário navega para aquela rota. Seu bundle inicial permanece pequeno, e seu Time to Interactive cai significativamente.
Se você estiver usando Next.js, você obtém code splitting baseado em rota gratuitamente com o App Router. Mas você ainda precisa ser intencional sobre o splitting em nível de componente para bibliotecas de terceiros pesadas:
import dynamic from "next/dynamic";
const RichTextEditor = dynamic(() => import("@/components/RichTextEditor"), {
loading: () => <EditorSkeleton />,
ssr: false,
});
Isso é crítico para bibliotecas como editores de rich text, bibliotecas de gráficos ou renderizadores de PDF que podem facilmente adicionar 200-500 KB ao seu bundle.
3. Estado Posicionado Muito Alto na Árvore de Componentes
O modelo de renderização do React é de cima para baixo. Quando o estado de um componente muda, ele re-renderiza — e toda a sua subárvore também. Quando você eleva o estado para o topo da árvore "por conveniência", você está dizendo ao React para re-renderizar tudo abaixo dele a cada mudança de estado.
O problema:
function App() {
const [selectedFilter, setSelectedFilter] = useState("all");
const [isModalOpen, setIsModalOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
return (
<Layout>
<Header />
<Sidebar filter={selectedFilter} onFilterChange={setSelectedFilter} />
<MainContent>
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<ProductList filter={selectedFilter} />
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</MainContent>
<Footer />
</Layout>
);
}
Digitar na barra de pesquisa aciona uma atualização de estado em App. Isso re-renderiza Header, Sidebar, Footer, ProductList e Modal — nenhum dos quais se importa com a consulta de pesquisa.
A solução:
Empurre o estado para onde ele é realmente usado:
function App() {
return (
<Layout>
<Header />
<SidebarWithFilter />
<MainContent>
<SearchSection />
<ProductList />
</MainContent>
<Footer />
</Layout>
);
}
function SearchSection() {
const [searchQuery, setSearchQuery] = useState("");
return (
<div>
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<SearchResults query={searchQuery} />
</div>
);
}
Agora, digitar na barra de pesquisa apenas re-renderiza SearchSection e seus filhos. Todo o resto permanece intocado.
O princípio é simples: o estado deve viver o mais próximo possível dos componentes que o leem. Se dois componentes irmãos precisam do mesmo estado, eleve-o para o pai comum mais próximo — mas não mais alto. Para estado verdadeiramente global (autenticação, tema, localidade), use uma biblioteca de gerenciamento de estado que suporte assinaturas seletivas, como Zustand ou Jotai, para que apenas os componentes que consomem uma fatia específica do estado re-renderizem quando essa fatia mudar.
4. Imagens Não Otimizadas no Next.js
Imagens são tipicamente os ativos mais pesados em uma página web. Já vi aplicativos Next.js onde os desenvolvedores usam tags <img> simples com PNGs enormes e não compactados. O resultado: uma página que carrega 8 MB de imagens, zero lazy loading, sem dimensionamento responsivo e um tempo de Largest Contentful Paint que faz o Lighthouse chorar.
O problema:
function ProjectCard({ project }) {
return (
<div className="card">
<img src={`/images/${project.cover}`} alt={project.title} />
<h3>{project.title}</h3>
<p>{project.description}</p>
</div>
);
}
Isso serve a imagem em resolução total no seu tamanho original, independentemente da viewport ou dispositivo do usuário. Sem conversão de formato, sem lazy loading, sem srcset responsivo.
A solução:
import Image from "next/image";
function ProjectCard({ project }) {
return (
<div className="card">
<Image
src={`/images/${project.cover}`}
alt={project.title}
width={600}
height={400}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
placeholder="blur"
blurDataURL={project.blurHash}
/>
<h3>{project.title}</h3>
<p>{project.description}</p>
</div>
);
}
O componente Image do Next.js oferece várias coisas automaticamente:
- Conversão automática de formato para WebP ou AVIF com base no suporte do navegador
- Dimensionamento responsivo via o atributo
sizes— o navegador baixa apenas o tamanho que precisa - Lazy loading por padrão — imagens abaixo da dobra não são buscadas até que o usuário role perto delas
- Placeholder de desfoque — mostra uma prévia leve enquanto a imagem completa carrega, eliminando o layout shift
Para imagens acima da dobra (banners de herói, conteúdo principal), adicione priority para desabilitar o lazy loading e pré-carregá-las:
<Image src={heroImage} alt="Hero" priority sizes="100vw" />
Essa única mudança pode reduzir o peso da página em 60-80% e melhorar drasticamente suas pontuações de Core Web Vitals.
5. Bloqueando o Thread Principal com Computações Pesadas
O thread principal do navegador lida com renderização, entrada do usuário, animações e execução de JavaScript — tudo em um único thread. Quando você executa uma computação pesada de forma síncrona, todo o resto congela. O usuário clica em um botão e nada acontece por 500ms. Animações travam. A rolagem para de responder.
O problema:
function AnalyticsDashboard({ rawData }) {
const [processedData, setProcessedData] = useState(null);
useEffect(() => {
// This blocks the main thread for 200-800ms
const result = processLargeDataset(rawData); // Sorting, filtering, aggregating 50k+ rows
setProcessedData(result);
}, [rawData]);
if (!processedData) return <Spinner />;
return <Charts data={processedData} />;
}
processLargeDataset é executado de forma síncrona no thread principal. Se levar 400ms, toda a UI fica congelada por 400ms. O usuário não pode rolar, digitar ou interagir com nada.
A solução — mova para um Web Worker:
// workers/dataProcessor.ts
self.onmessage = (event: MessageEvent) => {
const rawData = event.data;
const result = processLargeDataset(rawData);
self.postMessage(result);
};
function AnalyticsDashboard({ rawData }) {
const [processedData, setProcessedData] = useState(null);
const workerRef = useRef<Worker | null>(null);
useEffect(() => {
workerRef.current = new Worker(
new URL("../workers/dataProcessor.ts", import.meta.url)
);
workerRef.current.onmessage = (event: MessageEvent) => {
setProcessedData(event.data);
};
workerRef.current.postMessage(rawData);
return () => {
workerRef.current?.terminate();
};
}, [rawData]);
if (!processedData) return <Spinner />;
return <Charts data={processedData} />;
}
A computação pesada agora é executada em um thread separado. O thread principal permanece livre para lidar com interações do usuário, animações e renderização. A UI permanece responsiva o tempo todo.
Para casos menos extremos onde a computação leva 50-150ms, você pode dividir o trabalho em pedaços menores usando requestIdleCallback ou scheduler.yield() em vez de iniciar um Worker completo. Mas para qualquer coisa que processe grandes conjuntos de dados, gere relatórios ou execute algoritmos complexos, Web Workers não são opcionais — eles são necessários.
O Efeito Composto
Nenhum desses erros irá travar seu aplicativo por si só. É isso que os torna perigosos. Cada um adiciona 100-300ms de atraso, e os usuários experimentam a soma total. Um componente que re-renderiza desnecessariamente mais um bundle inchado mais imagens não otimizadas mais uma computação no thread principal significa que seu aplicativo leva 4 segundos para se tornar interativo em vez de 1.
Faça um perfil antes de otimizar. Use o React DevTools Profiler para encontrar pontos críticos de re-renderização. Use Lighthouse e Web Vitals para medir o impacto real no usuário. Use a aba Rede do seu navegador para identificar bundles e imagens superdimensionados. Use a aba Performance para identificar tarefas longas que bloqueiam o thread principal.
Esses são padrões que aplico em todos os projetos que construo — de plataformas de restaurantes a sistemas SaaS médicos. Performance não é um recurso que você adiciona depois. É uma qualidade que você mantém desde o primeiro commit.
Projetos Relacionados
RestoHub
Restaurantes param de perder 30% para o Uber Eats — ganham seu próprio sistema de pedidos, cardápio, site e programa de fidelidade em uma única plataforma. Experiência completa no estilo Uber Eats, mas o restaurante fica com cada centavo.
Poulet Rouge
Construí a presença digital completa do Poulet Rouge — uma rede de restaurantes com mais de 100 unidades em Quebec. Localizador de lojas interativo, widgets de pedidos online, calculadora nutricional e SEO bilíngue que realmente rankeia no Google.