Desempenho React
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). Esta skill organiza regras por prioridade e fornece orientação baseada em árvore de decisão para revisão de código ativa 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 no Lighthouse/Core Web Vitals
- 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 envolvem
app/,pages/,components/ou camadas de dados
Índice de Prioridade
| Prioridade | Categoria | Prefixo | Quando importa |
|---|---|---|---|
| 1 — CRÍTICA | Eliminando Waterfalls | async- | Sempre que await é seguido por await independente |
| 2 — CRÍTICA | Otimização de Tamanho do Bundle | bundle- | JS de carregamento inicial, importações por rota, bibliotecas de terceiros |
| 3 — ALTA | Desempenho no Lado do Servidor | server- | RSC, Server Actions, rotas de API, SSR |
| 4 — MÉDIA-ALTA | Busca de Dados no Cliente | client- | SWR / TanStack Query / fetch puro em hooks |
| 5 — MÉDIA | Otimização de Re-renderização | rerender- | Atualizações de estado de alta frequência, propagação pai-filho |
| 6 — MÉDIA | Desempenho de Renderização | rendering- | Listas longas, animações, hidratação |
| 7 — BAIXA-MÉDIA | Desempenho JavaScript | js- | Loops intensos, alocações frequentes |
| 8 — BAIXA | Padrões Avançados | advanced- | Integração efeito-evento, referências estáveis |
1. Eliminando Waterfalls (CRÍTICA)
"Waterfalls são o maior assassino de 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 — curto-circuito na 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é o uso
Mova 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 — inicie cedo, aguarde tarde
// CORRETO — inicie todas as promises, aguarde cada resultado apenas quando 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 limites de <Suspense> próximos dos dados para que a página renderize o que puder enquanto subárvores mais lentas chegam via streaming. A compensação: mudança de layout quando o conteúdo chega — reserve espaço (skeleton ou min-height).
Server Components: paralelo através de composição
// INCORRETO — awaits irmãos executam sequencialmente dentro de um componente
export default 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 de Tamanho do Bundle (CRÍTICA)
Importações diretas, não barris
Arquivos index.ts de barris forçam o bundler a percorrer todo o grafo de módulos, mesmo que o tree-shaking remova a maior parte. Importações diretas 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; importações diretas manuais ainda são necessárias para libs 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");
Importações dinâmicas para componentes pesados
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton />,
ssr: false, // quando somente cliente
});
Adie scripts de terceiros
Carregue análises, logging, 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 em cache quando o usuário clicar.
3. Desempenho no Lado do Servidor (ALTA)
Autentique Server Actions como rotas de API
Toda função "use server" é um endpoint público. Autentique E autorize dentro da ação — nunca confie na proteção do Client Component chamador.
"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 desduplica dentro de uma única requisição. Chamar getUser("1") de três Server Components na mesma renderização = uma consulta ao DB.
Cache LRU para dados entre requisições
Para dados que NÃO mudam por requisição (config, tabelas de consulta), faça 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 nível 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 com escopo de requisição (headers(), cookies(), contexto assíncrono) como alternativa.
Minimize dados passados para Client Components
Serializa apenas o que o Client precisa. Remova campos, pagine, projete colunas na camada do DB.
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, cache warming, 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ÉDIA-ALTA)
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.
Desduplicar listeners de eventos globais
// INCORRETO — cada componente adiciona o seu
useEffect(() => {
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
// CORRETO — ú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 da rolagem; o listener não pode chamar preventDefault().
localStorage: versão + minimização
- Sempre armazene um campo
version; incremente na mudança de schema e migre ou descarte dados antigos - Mantenha payloads pequenos —
localStorageé síncrono e bloqueia a thread principal
5. Otimização de Re-renderização (MÉDIA)
Não se inscreva em 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 renderização 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 renderização
useEffect(() => {}, [{ id, name }]);
// CORRETO — primitivas
useEffect(() => {}, [id, name]);
Inscreva-se em booleanos derivados, não em 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 o estado de vazio 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 primitivas simples
useMemo(() => x + 1, [x]) é overhead. Memo vale a pena para identidade de objeto e computação cara.
Divida hooks com dependências 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 sempre que as dependências 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 com frequência mas não devem disparar re-renderização (timestamps, última tecla, acumuladores).
Não defina componentes dentro de componentes
// INCORRETO — Inner é um novo componente em cada renderização de Outer
function Outer() {
const Inner = () => <span />;
return <Inner />;
}
Cada renderização cria um novo tipo Inner, derrotando a reconciliação e desmontando filhos.
6. Desempenho de Renderização (MÉDIA)
Anime o wrapper, não o SVG
Transformar um wrapper <div> em torno de um SVG é acelerado por GPU; transformar o SVG em si dispara pintura.
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 de 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, locale), coloque inline um <script> que define document.documentElement.dataset.* antes de o React montar.
Suprima warnings de hidratação esperados de forma restrita
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
Use APENAS para nós folha sabidamente divergentes — nunca em uma árvore contendo outros filhos.
<Activity> para mostrar/esconder em vez de montar/desmontar
React 19 <Activity mode="visible|hidden"> mantém o estado da árvore e effects montados mas oculta — mais barato que desmontar/remontar para abas e accordions.
Ternário em vez de && 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
Combine startTransition com a ação; React mostra a UI anterior como isPending enquanto o próximo estado é calculado.
Dicas de recurso React DOM
import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com");