Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Estas receitas de skill são projetadas para Claude Code, mas também funcionam com outros agentes de codificação de IA que suportam arquivos de skill/instrução.
O conteúdo completo do SKILL.md que você pode copiar para .claude/skills/vercel-react-best-practices/SKILL.md:
---
name: vercel-react-best-practices
description: "React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, based on Vercel Engineering guidelines. Contains 69 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Eliminating Waterfalls | CRITICAL | async- |
| 2 | Bundle Size Optimization | CRITICAL | bundle- |
| 3 | Server-Side Performance | HIGH | server- |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | client- |
| 5 | Re-render Optimization | MEDIUM | rerender- |
| 6 | Rendering Performance | MEDIUM | rendering- |
| 7 | JavaScript Performance | LOW-MEDIUM | js- |
| 8 | Advanced Patterns | LOW | advanced- |
---
## Category 1: Eliminating Waterfalls (CRITICAL)
### async-cheap-condition-before-await
Check cheap sync conditions before awaiting flags or remote values. If a condition can be checked synchronously, do it before the async call.
```tsx
// Bad: always awaits even when not needed
async function getContent(slug: string) \{
const flags = await getFeatureFlags();
if (!flags.newContent) return legacyContent(slug);
return newContent(slug);
\}
// Good: check cheap condition first
async function getContent(slug: string) \{
if (isLegacySlug(slug)) return legacyContent(slug); // sync check first
const flags = await getFeatureFlags();
if (!flags.newContent) return legacyContent(slug);
return newContent(slug);
\}
```
### async-defer-await
Move `await` into branches where the value is actually used. Do not await at the top when only some branches need the result.
```tsx
// Bad: always awaits
async function handler(req: Request) \{
const user = await getUser(req);
if (req.method === "OPTIONS") return new Response(null, \{ status: 204 \});
return new Response(JSON.stringify(user));
\}
// Good: defer await to where needed
async function handler(req: Request) \{
if (req.method === "OPTIONS") return new Response(null, \{ status: 204 \});
const user = await getUser(req);
return new Response(JSON.stringify(user));
\}
```
### async-parallel
Use `Promise.all()` for independent operations. Never sequentially await independent promises.
```tsx
// Bad: sequential (waterfall)
const user = await getUser(id);
const posts = await getPosts(id);
const stats = await getStats(id);
// Good: parallel
const [user, posts, stats] = await Promise.all([
getUser(id),
getPosts(id),
getStats(id),
]);
```
### async-suspense-boundaries
Use Suspense boundaries to stream content. Each independent data-fetching section should have its own Suspense boundary.
```tsx
// Bad: one boundary, everything waits
<Suspense fallback=\{<Loading />\}>
<SlowSection />
<FastSection />
</Suspense>
// Good: independent streaming
<Suspense fallback=\{<Skeleton />\}>
<FastSection />
</Suspense>
<Suspense fallback=\{<Skeleton />\}>
<SlowSection />
</Suspense>
```
---
## Category 2: Bundle Size Optimization (CRITICAL)
### bundle-barrel-imports
Import directly from the module, not through barrel (index) files. Barrel files prevent tree-shaking.
```tsx
// Bad: pulls in entire barrel
import \{ Button \} from "@/components";
// Good: direct import
import \{ Button \} from "@/components/ui/button";
```
### bundle-dynamic-imports
Use `next/dynamic` for heavy components that are not needed on initial render.
```tsx
import dynamic from "next/dynamic";
const Chart = dynamic(() => import("./chart"), \{
loading: () => <Skeleton className="h-64" />,
\});
```
### bundle-defer-third-party
Load analytics, logging, and non-critical scripts after hydration.
```tsx
useEffect(() => \{
import("./analytics").then((mod) => mod.init());
\}, []);
```
### bundle-conditional
Load modules only when a feature is activated.
```tsx
async function handleExport() \{
const \{ exportToPDF \} = await import("./pdf-export");
await exportToPDF(data);
\}
```
### bundle-preload
Preload on hover/focus for perceived speed.
```tsx
<Link
href="/dashboard"
onMouseEnter=\{() => router.prefetch("/dashboard")\}
>
Dashboard
</Link>
```
---
## Category 3: Server-Side Performance (HIGH)
### server-auth-actions
Authenticate server actions like API routes. Every Server Action is a public endpoint.
```tsx
"use server";
export async function deletePost(id: string) \{
const session = await auth();
if (!session) throw new Error("Unauthorized");
await db.post.delete(\{ where: \{ id \} \});
\}
```
### server-cache-react
Use `React.cache()` for per-request deduplication of expensive computations.
```tsx
import \{ cache \} from "react";
export const getUser = cache(async (id: string) => \{
return db.user.findUnique(\{ where: \{ id \} \});
\});
```
### server-dedup-props
Avoid duplicate serialization in RSC props. Fetch data in the component that needs it rather than passing large objects through multiple layers.
### server-serialization
Minimize data passed to client components. Only pass the fields the client component actually needs.
```tsx
// Bad: passes entire user object
<ClientProfile user=\{user\} />
// Good: passes only needed fields
<ClientProfile name=\{user.name\} avatar=\{user.avatar\} />
```
### server-parallel-fetching
Restructure components to parallelize fetches. Each Server Component fetches its own data independently.
### server-after-nonblocking
Use `after()` for non-blocking operations like logging and analytics.
```tsx
import \{ after \} from "next/server";
export async function POST(request: Request) \{
const data = await processRequest(request);
after(() => \{
logAnalytics(\{ event: "processed", data: data.id \});
\});
return Response.json(data);
\}
```
---
## Category 4: Client-Side Data Fetching (MEDIUM-HIGH)
### client-swr-dedup
Use SWR for automatic request deduplication. Multiple components using the same key share one request.
### client-passive-event-listeners
Use passive listeners for scroll and touch events to avoid blocking the main thread.
```tsx
element.addEventListener("scroll", handler, \{ passive: true \});
```
### client-localstorage-schema
Version and minimize localStorage data. Parse with validation on read.
---
## Category 5: Re-render Optimization (MEDIUM)
### rerender-defer-reads
Do not subscribe to state that is only used inside event handlers.
```tsx
// Bad: re-renders on every count change
function Component() \{
const count = useStore((s) => s.count);
return <button onClick=\{() => console.log(count)\}>Log</button>;
\}
// Good: read in the handler
function Component() \{
return (
<button onClick=\{() => console.log(useStore.getState().count)\}>
Log
</button>
);
\}
```
### rerender-memo
Extract expensive child trees into memoized components.
### rerender-derived-state-no-effect
Derive state during render, not in effects.
```tsx
// Bad: useState + useEffect
const [filtered, setFiltered] = useState(items);
useEffect(() => \{
setFiltered(items.filter(predicate));
\}, [items, predicate]);
// Good: derive during render
const filtered = useMemo(() => items.filter(predicate), [items, predicate]);
```
### rerender-functional-setstate
Use functional setState for stable callbacks that do not need the current value in scope.
```tsx
// Bad: depends on count, unstable
const increment = () => setCount(count + 1);
// Good: stable, no dependency on count
const increment = () => setCount((prev) => prev + 1);
```
### rerender-lazy-state-init
Pass a function to useState for expensive initial values.
```tsx
// Bad: runs on every render
const [data, setData] = useState(expensiveComputation());
// Good: runs only on mount
const [data, setData] = useState(() => expensiveComputation());
```
### rerender-no-inline-components
Never define components inside other components.
```tsx
// Bad: InnerList is recreated every render, loses state
function Parent() \{
const InnerList = () => <ul>\{items.map(...)\}</ul>;
return <InnerList />;
\}
// Good: defined outside
function InnerList(\{ items \}) \{
return <ul>\{items.map(...)\}</ul>;
\}
function Parent() \{
return <InnerList items=\{items\} />;
\}
```
### rerender-transitions
Use `startTransition` for non-urgent updates to keep the UI responsive.
### rerender-use-deferred-value
Defer expensive renders to keep input responsive.
---
## Category 6: Rendering Performance (MEDIUM)
### rendering-content-visibility
Use `content-visibility: auto` for long lists and off-screen content.
```css
.card \{
content-visibility: auto;
contain-intrinsic-size: 0 200px;
\}
```
### rendering-hoist-jsx
Extract static JSX outside components to avoid recreating on every render.
### rendering-conditional-render
Use ternary, not `&&` for conditional rendering to avoid rendering `0` or `false`.
```tsx
// Risky: renders "0" when count is 0
\{count && <Badge count=\{count\} />\}
// Safe: explicit ternary
\{count > 0 ? <Badge count=\{count\} /> : null\}
```
### rendering-resource-hints
Use React DOM resource hints for preloading critical resources.
```tsx
import \{ preload, preconnect \} from "react-dom";
preload("/fonts/inter.woff2", \{ as: "font", type: "font/woff2" \});
preconnect("https://api.example.com");
```
---
## Category 7: JavaScript Performance (LOW-MEDIUM)
### js-batch-dom-css
Group CSS changes via classes or cssText instead of individual style property changes.
### js-index-maps
Build a Map for repeated lookups instead of using array.find() in a loop.
### js-set-map-lookups
Use Set for O(1) membership checks instead of array.includes().
```tsx
// Bad: O(n) per check
const isAdmin = adminIds.includes(userId);
// Good: O(1) per check
const adminSet = new Set(adminIds);
const isAdmin = adminSet.has(userId);
```
### js-combine-iterations
Combine multiple filter/map into one loop or use flatMap.
### js-early-exit
Return early from functions to avoid unnecessary computation.
---
## Category 8: Advanced Patterns (LOW)
### advanced-event-handler-refs
Store event handlers in refs to avoid re-subscribing effects.
```tsx
const handlerRef = useRef(onMessage);
handlerRef.current = onMessage;
useEffect(() => \{
const handler = (e: MessageEvent) => handlerRef.current(e);
window.addEventListener("message", handler);
return () => window.removeEventListener("message", handler);
\}, []); // never re-runs
```
### advanced-init-once
Initialize app-level setup exactly once per application load.
### advanced-use-latest
useLatest pattern for stable callback refs that always point to the latest closure.
---
## How to Apply Rules
When reviewing or writing code:
1. Start with CRITICAL rules (Categories 1-2) as they have the highest impact
2. Apply HIGH rules (Category 3) for server-side code
3. Check MEDIUM rules (Categories 4-6) for client-side components
4. Consider LOW rules (Categories 7-8) for hot paths and performance-critical code
When generating new code, always follow rules from Categories 1-3 by default. Only apply lower-priority rules when specifically optimizing.Exemplo de prompt: "Revise este componente para problemas de performance"
// User's component
"use client";
import { useState, useEffect } from "react";
import { icons } from "lucide-react"; // barrel import
export default function Dashboard() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch("/api/user").then(r => r.json()).then(setUser);
}, []);
useEffect(() => {
if (user) fetch("/api/posts").then(r => r.json()).then(setPosts);
}, [user]);
const filtered = posts.filter(p => p.published);
return (
<div>
{user && <h1>{user.name}</h1>}
{filtered.map((post, i) => (
<div key={i} style={{ padding: "10px" }}>
<p onClick={() => console.log(post.id)}>{post.title}</p>
</div>
))}
</div>
);
}Exemplo de saída da skill:
A skill identificaria as seguintes violações:
| Regra | Severidade | Problema |
|---|---|---|
| server- (Categoria 3) | CRÍTICO | Página inteira é "use client" com useEffect buscando dados. Deveria ser um Server Component |
| async-parallel | CRÍTICO | Waterfall sequencial: busca de usuário e depois busca de posts |
| bundle-barrel-imports | CRÍTICO | Importar de barrel lucide-react puxa a biblioteca inteira de ícones |
| rerender-no-inline (Categoria 5) | MÉDIO | Objetos de estilo inline recriados a cada renderização |
| rerender- (Categoria 5) | MÉDIO | Índice de array como chave em lista dinâmica |
E forneceria a versão refatorada:
// app/dashboard/page.tsx (Server Component - no "use client")
import { getUser, getPosts } from "@/lib/data";
export default async function Dashboard() {
const [user, posts] = await Promise.all([getUser(), getPosts()]);
const filtered = posts.filter(p => p.published);
return (
<div>
<h1>{user.name}</h1>
{filtered.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}mkdir -p .claude/skills/vercel-react-best-practices
# Copie a seção Receita acima para:
# .claude/skills/vercel-react-best-practices/SKILL.mdEsta skill funciona bem combinada com:
Não aplique todas as 69 regras de uma vez. Comece com CRÍTICAS (Categorias 1-2) para o maior ROI. Regras de menor prioridade importam menos até que as críticas sejam abordadas.
O React Compiler muda as regras de memoização. Se estiver usando o React Compiler, as regras da Categoria 5 (rerender-) sobre useMemo/useCallback manual se tornam menos relevantes. O compilador cuida da maioria delas automaticamente.
Algumas regras entram em conflito com a legibilidade. Regras como js-combine-iterations (combinando filter+map em um único loop) podem prejudicar a legibilidade para um ganho de performance mínimo. Aplique apenas em caminhos quentes.
Regras de Server Component aplicam-se apenas ao App Router do Next.js. Se estiver usando Pages Router ou React puro, as regras da Categoria 3 (server-) não se aplicam.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| ESLint react-hooks/exhaustive-deps | Verificações automatizadas em tempo de lint para dependências de hook | Precisa de revisão de performance mais ampla |
| @next/bundle-analyzer | Analisando a saída real do bundle | Precisa de verificação de regras em nível de código |
| React DevTools Profiler | Profiling em tempo de execução de renderizações reais | Quer análise estática pré-commit |
| Lighthouse CI | Verificações CWV automatizadas em CI | Precisa de recomendações em nível de código |
// Bad: sequential (waterfall)
const user = await getUser(id);
const posts = await getPosts(id);
// Good: parallel
const [user, posts] = await Promise.all([
getUser(id),
getPosts(id),
]);Promise.all() para operações assíncronas independentesimport { Button } from "@/components" puxa o barrel inteiroimport { Button } from "@/components/ui/button" importa apenas o que é necessário// Bad: re-renders on every count change
const count = useStore((s) => s.count);
return <button onClick={() => console.log(count)}>Log</button>;
// Good: read state only inside the handler
return (
<button onClick={() => console.log(useStore.getState().count)}>
Log
</button>
);await auth() no início de cada Server Action// Risky: renders "0" when count is 0
{count && <Badge count={count} />}
// Safe: explicit ternary
{count > 0 ? <Badge count={count} /> : null}&& com um número pode renderizar 0 ou false como texto visível// Bad: runs expensive computation on every render
const [data, setData] = useState(expensiveComputation());
// Good: runs only on mount
const [data, setData] = useState(() => expensiveComputation());useState para que o valor inicial caro seja computado apenas uma vezuseMemo, useCallback e React.memo se tornam menos relevantes// Bad: passes entire user object to client
<ClientProfile user={user} />
// Good: passes only needed fields
<ClientProfile name={user.name} avatar={user.avatar} />await para o branch onde o valor é realmente usado// Bad: O(n) per check
const isAdmin = adminIds.includes(userId);
// Good: O(1) per check
const adminSet = new Set(adminIds);
const isAdmin = adminSet.has(userId);Set para verificações de pertencimento O(1) em vez de Array.includes()Map para buscas repetidas em vez de Array.find() em loopsRevisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥