Noções Básicas de Desempenho
13 exemplos para você começar com Desempenho do React -- 9 básicos e 4 intermediários.
Busque em todas as páginas da documentação
13 exemplos para você começar com Desempenho do React -- 9 básicos e 4 intermediários.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Todos os exemplos assumem um projeto Next.js 15+ App Router com React 19 e TypeScript. Alguns exemplos usam ferramentas extras:
@next/bundle-analyzer para inspeção de pacotes: npm install --save-dev @next/bundle-analyzer.web-vitals para monitoramento de Core Web Vitals: npm install web-vitals.zustand para o exemplo de desempenho de estado: npm install zustand.Duas regras orientadoras para cada exemplo abaixo:
memo, useMemo, useCallback manuais apenas depois que o profiling provar o ganho.Procurando uma revisão sistemática? Veja a Lista de Verificação de Desempenho -- uma auditoria de 30 pontos que você pode executar em CI.
Use o React DevTools para gravar uma interação real e ver quais componentes realmente re-renderizam.
import { Profiler, type ProfilerOnRenderCallback } from "react";
const onRender: ProfilerOnRenderCallback = (id, phase, actualDuration) => {
console.log(`[${id}] ${phase} levou ${actualDuration.toFixed(1)}ms`);
};
export default function Page() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>
);
}
function Dashboard() {
return <p>Conteúdo</p>;
}<Profiler> embutido registra tempos de renderização para sua subárvore -- ótimo para medições pontuais.actualDuration vs baseDuration -- grandes diferenças entre eles destacam re-renderizações desperdiçadas que você pode memoizar.<Profiler> para produção -- ele tem sobrecarga mensurável. Envolva seu uso em uma verificação apenas para desenvolvimento se ele viver na árvore.Relacionado: React DevTools Profiler -- gráficos de chama, inspeção de commit, interações | Lista de Verificação de Desempenho -- o que medir em CI
Uma re-renderização do pai não precisa se propagar -- divida o estado e mantenha referências estáveis para que os filhos pulem o trabalho.
"use client";
import { memo, useState } from "react";
const Row = memo(function Row({ label }: { label: string }) {
console.log("Render", label);
return <li>{label}</li>;
});
export default function List({ items }: { items: string[] }) {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>{count}</button>
<ul>
{items.map((i) => (
<Row key={i} label={i} />
))}
</ul>
</>
);
}memo(Component) pula re-renderizações quando as props são referencialmente iguais à última renderização.List, mas as props de Row (label) não mudaram, então cada linha é pulada.style={{ ... }}) e funções inline quebram memo -- sua referência muda a cada renderização.key em listas para permitir que o React reutilize nós DOM quando os itens se movem -- um key={index} em uma lista reordenada é uma fonte frequente de bugs.Relacionado: Prevenindo Re-renderizações Desnecessárias -- dividir estado, elevar para baixo, chaves | Memoização -- os primitivos por trás deste padrão
Estabilize valores caros e referências de função para que os filhos memoizados realmente se beneficiem.
"use client";
import { memo, useCallback, useMemo, useState } from "react";
const ExpensiveChart = memo(function ExpensiveChart({
points,
onPick,
}: {
points: number[];
onPick: (n: number) => void;
}) {
return <p>{points.length} pontos</p>;
});
export default function Dashboard({ raw }: { raw: number[] }) {
const [picked, setPicked] = useState<number | null>(null);
const points = useMemo(() => raw.filter((n) => n > 0), [raw]);
const onPick = useCallback((n: number) => setPicked(n), []);
return (
<>
<p>Escolhido: {picked ?? "nenhum"}</p>
<ExpensiveChart points={points} onPick={onPick} />
</>
);
}useMemo(fn, deps) armazena o valor em cache; useCallback(fn, deps) armazena a referência da função em cache -- ambos mantêm a identidade da prop estável para filhos memoizados.raw.filter(...) e (n) => setPicked(n) seriam novas referências a cada renderização, invalidando memo.Relacionado: Memoização -- quando a memoização realmente ajuda | useMemo / useCallback -- as APIs de hook
Ative o React Compiler e deixe-o inserir a memoização para você -- exclua a maioria dos memo/useMemo/useCallback manuais.
// next.config.ts
import type { NextConfig } from "next";
const config: NextConfig = {
experimental: {
reactCompiler: true,
},
};
export default config;// Após o compilador estar ativado, escreva componentes simples:
function ProductList({ products }: { products: Product[] }) {
const total = products.reduce((sum, p) => sum + p.price, 0);
const handleClick = (id: string) => console.log(id);
return <p>{total} ({products.length} itens)</p>;
}useMemo/useCallback existentes continuam funcionando.npx react-compiler-healthcheck valida sua base de código.Relacionado: React Compiler -- configuração, bailouts, depuração | React Compiler (React 19) -- a página de recursos do React 19
Adie o carregamento de um componente pesado até que o usuário realmente precise dele.
"use client";
import dynamic from "next/dynamic";
import { useState } from "react";
const Chart = dynamic(() => import("./Chart"), {
loading: () => <p>Carregando gráfico...</p>,
ssr: false,
});
export default function Dashboard() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>Mostrar gráfico</button>
{open && <Chart />}
</>
);
}next/dynamic retorna um componente que divide o código em um chunk separado; ele baixa apenas quando renderizado.open && <Chart />) para que o chunk seja buscado sob demanda -- ideal para modais, gráficos e editores de texto ricos.ssr: false desativa a renderização do servidor quando o componente depende de APIs exclusivas do navegador (window, document).Relacionado: Otimização de Tamanho de Pacote -- analisadores, tree-shaking, custo de pacotes | Desempenho de Imagem e Fonte -- outras maneiras de economizar bytes
Use Server Components por padrão; remova "use client" apenas onde a interatividade começa.
// app/dashboard/page.tsx -- um Server Component
import { ClientChart } from "./chart";
interface Product {
id: number;
name: string;
price: number;
}
export default async function DashboardPage() {
const res = await fetch("https://api.example.com/products");
const products: Product[] = await res.json();
const total = products.reduce((sum, p) => sum + p.price, 0);
return (
<>
<h1>Total: ${total}</h1>
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
<ClientChart data={products} />
</>
);
}<ClientChart> interativo é o único JS enviado.Relacionado: Desempenho de Server Components -- fronteiras, padrões, medições | Server Components (React 19) -- o primitivo
Use o <Image> do Next.js para que o navegador envie o tamanho certo, carregue preguiçosamente abaixo da dobra e reserve espaço para evitar mudanças de layout.
import Image from "next/image";
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="Landing hero"
width={1200}
height={630}
priority
sizes="(max-width: 768px) 100vw, 1200px"
/>
);
}next/image serve formatos modernos (AVIF/WebP), gera múltiplos tamanhos e carrega preguiçosamente automaticamente.width e height reservam espaço -- mudança de layout zero mesmo antes da imagem chegar.priority desativa o carregamento preguiçoso para imagens acima da dobra para que elas comecem a carregar imediatamente (grande ganho de LCP).sizes informa ao navegador a largura que a imagem renderizará em cada breakpoint -- necessário para escolher a fonte correta.Relacionado: Desempenho de Imagem e Fonte -- carregamento de fontes, preload, imagens OG | next/image -- API completa de
<Image>
Envie métricas LCP, INP e CLS de usuários reais para análise para que você saiba o que o mundo real vê.
// app/web-vitals.tsx
"use client";
import { useReportWebVitals } from "next/web-vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric.name, metric.value);
// fetch("/api/metrics", { method: "POST", body: JSON.stringify(metric) });
});
return null;
}
// app/layout.tsx
// <WebVitals />useReportWebVitals dispara uma vez por métrica por página -- encaminhe-as para sua análise, Sentry ou Vercel Analytics.Relacionado: Otimização de Core Web Vitals -- correções e medições por métrica | Lista de Verificação de Desempenho -- portões de CI e orçamentos
Divida uma rota em fronteiras de streaming para que painéis rápidos renderizem enquanto os lentos ainda estão buscando.
// app/dashboard/page.tsx
import { Suspense } from "react";
import { FastStats } from "./fast-stats";
import { SlowChart } from "./slow-chart";
export default function Dashboard() {
return (
<div>
<Suspense fallback={<p>Carregando estatísticas...</p>}>
<FastStats />
</Suspense>
<Suspense fallback={<p>Carregando gráfico...</p>}>
<SlowChart />
</Suspense>
</div>
);
}<Suspense> transmite independentemente -- o usuário vê o painel rápido imediatamente em vez de esperar por toda a rota.loading.tsx para streaming em nível de rota e <Suspense> aninhado para streaming granular por painel.Promise.all) para que cada painel seja totalmente carregado no momento em que seu JS for transmitido.Relacionado: Desempenho de Suspense e Streaming -- estratégia de fronteira | Suspense (padrões) -- o primitivo | Streaming (Next.js Data) -- streaming em nível de rota
Assine uma fatia da loja para que apenas os componentes que leem essa fatia re-renderizem.
"use client";
import { create } from "zustand";
interface CartStore {
items: { id: string; qty: number }[];
count: number;
addItem: (id: string) => void;
}
const useCart = create<CartStore>((set) => ({
items: [],
count: 0,
addItem: (id) =>
set((s) => {
const items = [...s.items, { id, qty: 1 }];
return { items, count: items.length };
}),
}));
// Re-renderiza apenas quando `count` muda, não quando qualquer outro campo muda
function CartBadge() {
const count = useCart((s) => s.count);
return <span>{count}</span>;
}
// Re-renderiza apenas quando a referência `addItem` muda (nunca, a menos que a loja seja recriada)
function AddButton({ id }: { id: string }) {
const addItem = useCart((s) => s.addItem);
return <button onClick={() => addItem(id)}>Adicionar</button>;
}shallow ou retorne primitivos.Relacionado: Desempenho de Gerenciamento de Estado -- divisão de contexto, estado derivado | Seletores Zustand -- padrões de seletores mais profundos
Inicie buscas independentes em paralelo para que o tempo total de espera seja o mais lento, não a soma.
// app/dashboard/page.tsx
interface User { name: string; }
interface Stats { totalSales: number; }
interface Activity { events: string[]; }
async function getUser(): Promise<User> {
return (await fetch("https://api.example.com/me")).json();
}
async function getStats(): Promise<Stats> {
return (await fetch("https://api.example.com/stats")).json();
}
async function getActivity(): Promise<Activity> {
return (await fetch("https://api.example.com/activity")).json();
}
export default async function Dashboard() {
const [user, stats, activity] = await Promise.all([
getUser(),
getStats(),
getActivity(),
]);
return (
<p>
{user.name} - ${stats.totalSales} - {activity.events.length} eventos
</p>
);
}awaits sequenciais criam uma cascata -- cada requisição espera a anterior resolver.Promise.all dispara todas as requisições de uma vez; o tempo total é igual à requisição mais lenta.Promise.allSettled quando uma falha não deve rejeitar todo o conjunto (por exemplo, dados opcionais da barra lateral).Relacionado: Desempenho de Busca de Dados -- cascatas, paralelismo, cache | Promises Paralelas -- padrões Promise.all, allSettled
Sempre desfaça assinaturas, timers e listeners na limpeza do useEffect.
"use client";
import { useEffect, useState } from "react";
export default function LiveCounter() {
const [n, setN] = useState(0);
useEffect(() => {
const controller = new AbortController();
const id = setInterval(() => setN((x) => x + 1), 1000);
window.addEventListener("resize", () => console.log("resize"), {
signal: controller.signal,
});
return () => {
clearInterval(id);
controller.abort(); // remove o listener
};
}, []);
return <p>Ticks: {n}</p>;
}setInterval, setTimeout, addEventListener e assinatura deve ter uma limpeza correspondente, ou o navegador manterá o componente para sempre.AbortController é a maneira moderna de remover listeners -- um abort() desfaz todos os listeners registrados com o sinal.fetch em andamento com o mesmo sinal evita avisos de "setState após desmontagem".Relacionado: Vazamentos de Memória -- detecção, padrões WeakMap, refs de longa duração | useEffect -- regras e tempo de limpeza
Armazene em cache buscas caras entre requisições e invalide-as cirurgicamente quando os dados mudarem.
// app/lib/data.ts - helper apenas para servidor
import { unstable_cache } from "next/cache";
interface Post {
id: number;
title: string;
}
export const getPosts = unstable_cache(
async (): Promise<Post[]> => {
const res = await fetch("https://api.example.com/posts");
return res.json();
},
["posts"],
{ tags: ["posts"], revalidate: 300 },
);
// app/posts/actions.ts
"use server";
import { revalidateTag } from "next/cache";
export async function createPost(title: string) {
await fetch("https://api.example.com/posts", {
method: "POST",
body: JSON.stringify({ title }),
});
revalidateTag("posts");
}unstable_cache(fn, keys, options) memoiza o resultado entre requisições -- chamadores subsequentes obtêm um acerto de cache.tags: ["posts"] permite invalidar cada entrada em cache com essa tag em uma única chamada via revalidateTag("posts").revalidate: 300 adiciona um teto baseado em tempo de 5 minutos caso você esqueça de invalidar a tag em algum lugar.fetch simples já memoiza -- reserve unstable_cache para compartilhamento entre requisições.Relacionado: Aprofundamento no Cache do Next.js -- modelo de quatro camadas, ciclos de vida do cache | Cache (Next.js Data) -- opções de cache em nível de fetch | Revalidação -- revalidatePath vs revalidateTag vs ISR
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥