React Performance
Padrões de otimização de desempenho para React 18/19 e Next.js, adaptados de Vercel Labs react-best-practices (MIT, v1.0.0). Este skill organiza regras por prioridade e fornece orientação com árvore de decisão para revisão ativa de código e refatoração.
Quando Ativar
- Escrevendo ou revisando código React/Next.js para desempenho
- Diagnosticando carregamentos lentos de página, interações lentas ou alto uso de CPU no cliente
- Auditando tamanho do bundle ou regressões nas Core Web Vitals do Lighthouse
- Removendo waterfalls em Server Components / rotas de API
- Reduzindo re-renderizações no lado do cliente
- Otimizando listas longas, animações ou hidratação
- Auditando escolhas de otimização em PRs que mexem em
app/,pages/,components/ou camadas de dados
Índice de Prioridade
| Prioridade | Categoria | Prefixo | Quando importa |
|---|---|---|---|
| 1 — CRÍTICO | Eliminando Waterfalls | async- | Sempre que await é seguido por await independente |
| 2 — CRÍTICO | Otimização do Tamanho do Bundle | bundle- | JS de carregamento inicial, imports por rota, bibliotecas de terceiros |
| 3 — ALTO | Desempenho no Servidor | server- | RSC, Server Actions, rotas de API, SSR |
| 4 — MÉDIO-ALTO | Busca de Dados no Cliente | client- | SWR / TanStack Query / fetch bruto em hooks |
| 5 — MÉDIO | Otimização de Re-renderização | rerender- | Atualizações de estado de alta frequência, propagação pai-filho |
| 6 — MÉDIO | Desempenho de Renderização | rendering- | Listas longas, animações, hidratação |
| 7 — BAIXO-MÉDIO | Desempenho JavaScript | js- | Loops intensivos, alocações frequentes |
| 8 — BAIXO | Padrões Avançados | advanced- | Integração efeito-evento, refs estáveis |
1. Eliminando Waterfalls (CRÍTICO)
"Waterfalls são o maior inimigo do desempenho" — cada
awaitsequencial adiciona latência de rede completa.
Condições baratas antes de await
Verifique condições síncronas (props, env, flags fixas) antes de aguardar dados remotos.
// INCORRETO
async function Page({ id }: { id: string }) {
const flag = await getFlag("show-page");
if (!flag || !id) return null;
const data = await getData(id);
// ...
}
// CORRETO — interrompe antes com condição síncrona barata primeiro
async function Page({ id }: { id: string }) {
if (!id) return null;
const flag = await getFlag("show-page");
if (!flag) return null;
const data = await getData(id);
}
Adie awaits até serem usados
Mova o await para dentro do ramo que o utiliza.
// INCORRETO — aguarda antes de decidir se precisa dos dados
const user = await getUser(id);
if (mode === "guest") return renderGuest();
return renderUser(user);
// CORRETO
if (mode === "guest") return renderGuest();
const user = await getUser(id);
return renderUser(user);
Promise.all para trabalho independente
// INCORRETO — sequencial
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);
// CORRETO — paralelo
const [user, posts, followers] = await Promise.all([
getUser(id),
getPosts(id),
getFollowers(id),
]);
Dependências parciais — comece cedo, aguarde tarde
// CORRETO — inicia todas as promessas, aguarda apenas quando cada resultado é necessário
const userP = getUser(id);
const postsP = getPosts(id);
const profile = await getProfile(id);
if (profile.private) return null;
const [user, posts] = await Promise.all([userP, postsP]);
Suspense para streaming
Coloque os limites de <Suspense> perto dos dados para que a página renderize o que puder enquanto sub-árvores mais lentas são transmitidas. A compensação: mudança de layout quando o conteúdo chega — reserve espaço (skeleton ou min-height).
Server Components: paralelização através de composição
// INCORRETO — awaits irmãos executam sequencialmente dentro de um componente
async function Page() {
const user = await getUser();
const cart = await getCart();
return <View user={user} cart={cart} />;
}
// CORRETO — divida em filhos, React os executa em paralelo
export default async function Page() {
return (
<View>
<UserSection />
<CartSection />
</View>
);
}
2. Otimização do Tamanho do Bundle (CRÍTICO)
Imports diretos, não barris
Arquivos index.ts (barris) forçam o bundler a percorrer todo o grafo do módulo mesmo quando o tree-shaking remove a maior parte. Imports diretos economizam 200-800ms de JS de carregamento inicial em muitos aplicativos reais.
// INCORRETO
import { Button, Card, Modal } from "@/components";
// CORRETO
import { Button } from "@/components/Button";
import { Card } from "@/components/Card";
import { Modal } from "@/components/Modal";
Next.js 13.5+ tem Optimize Package Imports que automatiza isso para pacotes listados — use-o; imports diretos manuais ainda são necessários para bibliotecas não listadas.
Caminhos estaticamente analisáveis
// INCORRETO — impede análise do bundler/trace
const mod = await import(`./pages/${name}`);
// CORRETO — explícito por ramo
const mod = name === "home" ? await import("./pages/home") : await import("./pages/about");
Imports dinâmicos para componentes pesados
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton />,
ssr: false, // quando apenas cliente
});
Adie scripts de terceiros
Carregue analytics, logs, widgets de suporte APÓS a hidratação. Use next/script com strategy="afterInteractive" (padrão) ou "lazyOnload".
Carregamento condicional de módulos
if (user.role === "admin") {
const { AdminPanel } = await import("./admin/AdminPanel");
// ...
}
Pré-carregue ao passar o mouse/foco
Dispare <link rel="preload"> ou import() ao passar o mouse para que o bundle esteja no cache quando o usuário clicar.
3. Desempenho no Servidor (ALTO)
Autentique Server Actions como rotas de API
Toda função "use server" é um endpoint público. Autentique E autorize dentro da action — nunca dependa da proteção do Client Component que a chama.
"use server";
export async function deleteUser(formData: FormData) {
const session = await getSession();
if (!session?.user) throw new Error("Unauthorized");
const targetId = String(formData.get("id"));
if (session.user.role !== "admin" && session.user.id !== targetId) {
throw new Error("Forbidden");
}
await db.user.delete({ where: { id: targetId } });
}
React.cache() para desduplicação por requisição
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
React.cache remove duplicatas dentro de uma única requisição. Chamar getUser("1") de três Server Components na mesma renderização = uma consulta ao banco.
Cache LRU para dados entre requisições
Para dados que NÃO mudam por requisição (config, tabelas de consulta), armazene em cache fora do React com um cache LRU ou unstable_cache.
Evite serialização duplicada em props RSC
Quando um Server Component renderiza os mesmos dados em vários Client Components, os dados são serializados uma vez por consumidor. Eleve o Client Component e passe children.
Eleve E/S estática ao escopo do módulo
// CORRETO — executa uma vez no carregamento do módulo
const fontData = readFileSync(fontPath);
export async function Page() {
return <Banner font={fontData} />;
}
Sem estado mutável no escopo do módulo em RSC/SSR
Estado de módulo no servidor é compartilhado entre todas as requisições — uma condição de corrida entre usuários. Use armazenamento no escopo da requisição (headers(), cookies(), contexto assíncrono).
Minimize dados passados para Client Components
Serialize apenas o que o Cliente precisa. Remova campos, pagine, projete colunas na camada do banco de dados.
Paralelize buscas aninhadas com Promise.all por item
const users = await getUsers();
const enriched = await Promise.all(
users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })),
);
Use after() para trabalho não bloqueante
after() do Next.js 15 executa trabalho após o envio da resposta — logging, aquecimento de cache, analytics.
import { after } from "next/server";
export async function GET() {
const data = await getData();
after(() => logAnalytics(data));
return Response.json(data);
}
4. Busca de Dados no Cliente (MÉDIO-ALTO)
SWR / TanStack Query para desduplicação
Múltiplos componentes chamando useUser(id) devem compartilhar uma requisição de rede e uma entrada de cache. Use SWR ou TanStack Query — nunca crie seu próprio useEffect + fetch para dados compartilhados.
Desduplique listeners de eventos globais
// INCORRETO — cada componente adiciona o seu próprio
useEffect(() => {
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
// CORRETO — um único listener compartilhado via hook + subject global
const useScroll = createScrollHook(); // subject singleton por baixo dos panos
Listeners passivos para scroll
window.addEventListener("scroll", handler, { passive: true });
Melhora a suavidade do scroll; o listener não pode usar preventDefault().
localStorage: versão + minimizar
- Sempre armazene um campo
version; incremente em mudanças de esquema e migre ou descarte dados antigos - Mantenha os payloads pequenos —
localStorageé síncrono e bloqueia a thread principal
5. Otimização de Re-renderização (MÉDIO)
Não assine estado usado apenas em callbacks
// INCORRETO — re-renderiza toda vez que count muda
const count = useStore((s) => s.count);
const handler = () => doSomething(count);
// CORRETO — lê uma vez na chamada
const handler = () => {
const count = useStore.getState().count;
doSomething(count);
};
Extraia trabalho pesado para componentes memoizados
// CORRETO — filho re-renderiza apenas quando `items` muda
const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
return <Chart data={transform(items)} />;
});
Eleve props não primitivas padrão
// INCORRETO — novo array a cada render quebra memo
<List items={items ?? []} />
// CORRETO
const EMPTY: Item[] = [];
<List items={items ?? EMPTY} />
Dependências primitivas em effects
// INCORRETO — nova identidade de objeto a cada render
useEffect(() => {}, [{ id, name }]);
// CORRETO — primitivos
useEffect(() => {}, [id, name]);
Assine booleanos derivados, não valores brutos
// INCORRETO — re-renderiza para qualquer mudança no carrinho
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;
// CORRETO — re-renderiza apenas quando a vacância muda
const hasItems = useStore((s) => s.cart.length > 0);
Derive durante a renderização, nunca via useEffect
// INCORRETO
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// CORRETO
const full = `${first} ${last}`;
setState funcional para callbacks estáveis
// CORRETO
const increment = useCallback(() => setCount((c) => c + 1), []);
Inicializador de estado lazy para valores caros
const [tree] = useState(() => parseTree(largeInput));
Evite memo para primitivos simples
useMemo(() => x + 1, [x]) é overhead. Memo vale a pena para identidade de objeto e computação cara.
Divida hooks com deps independentes
// INCORRETO — ambos os seletores reexecutam se qualquer fonte mudar
const { a, b } = useSomething(source1, source2);
// CORRETO
const a = useA(source1);
const b = useB(source2);
Mova lógica de interação para event handlers
Event handlers executam apenas na ação do usuário — useEffect reexecuta quando deps mudam.
startTransition para atualizações não urgentes
const [pending, startTransition] = useTransition();
startTransition(() => setFilters(newFilters));
useDeferredValue para renderizações caras
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
useRef para valores transitórios frequentes
Para valores que mudam frequentemente mas não devem disparar re-renderização (timestamps, última tecla, acumuladores).
Não defina componentes dentro de componentes
// INCORRETO — Inner é um novo componente a cada render de Outer
function Outer() {
const Inner = () => <span />;
return <Inner />;
}
Cada render cria um novo tipo Inner, impedindo a reconciliação e desmontando filhos.
6. Desempenho de Renderização (MÉDIO)
Anima o wrapper, não o SVG
Transformar um <div> wrapper ao redor de um SVG é acelerado por GPU; transformar o SVG em si dispara paint.
content-visibility: auto para listas longas
.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }
O navegador pula a renderização fora da tela — grande ganho para listas com centenas de linhas.
Eleve JSX estático
const STATIC_HEADER = <h1>Title</h1>;
function Page() {
return <>{STATIC_HEADER}<Body /></>;
}
SVG: reduza a precisão das coordenadas
d="M10.123456,20.654321" → d="M10.12,20.65". Cada dígito custa bytes; a diferença visual é sub-pixel.
Hidratação sem flicker via script inline
Para valores necessários antes da hidratação (tema, localidade), insira um <script> inline que define document.documentElement.dataset.* antes do React montar.
Suprima incompatibilidades de hidratação esperadas de forma estreita
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
Use APENAS para nós-folha conhecidamente divergentes — nunca em uma árvore contendo outros filhos.
<Activity> para mostrar/esconder ao invés de montar/desmontar
React 19 <Activity mode="visible|hidden"> mantém o estado da árvore e effects montados, mas esconde — mais barato que desmontar/remontar para abas e accordions.
Ternário sobre && para renderização condicional
// INCORRETO — `0` renderiza como nó de texto
{count && <Badge>{count}</Badge>}
// CORRETO
{count > 0 ? <Badge>{count}</Badge> : null}
useTransition para estados de carregamento
Emparelhe startTransition com a ação; React mostra a UI anterior como isPending enquanto o próximo estado computa.
Dicas de recurso do React DOM
import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com")
…
[truncado para tradução]