Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Estas recetas de skill están diseñadas para Claude Code pero también funcionan con otros agentes de codificación con IA que admiten archivos de skill/instrucciones.
El contenido completo de SKILL.md que puedes copiar en .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.Prompt de ejemplo: "Revisa este componente en busca de problemas de rendimiento"
// Componente del usuario
"use client";
import { useState, useEffect } from "react";
import { icons } from "lucide-react"; // import barrel
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>
);
}Salida de ejemplo de la skill:
La skill identificaría estas violaciones:
| Regla | Severidad | Problema |
|---|---|---|
| server- (Categoría 3) | CRÍTICO | Toda la página es "use client" con useEffect para fetch. Debería ser un Server Component |
| async-parallel | CRÍTICO | Waterfall secuencial: fetch de user y luego fetch de posts |
| bundle-barrel-imports | CRÍTICO | Importar desde el barrel lucide-react arrastra toda la librería de iconos |
| rerender-no-inline (Categoría 5) | MEDIO | Objetos de estilo inline recreados en cada renderizado |
| rerender- (Categoría 5) | MEDIO | Índice del array como key en una lista dinámica |
Y proporcionaría la versión refactorizada:
// app/dashboard/page.tsx (Server Component - sin "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
# Copia la sección Receta de arriba en:
# .claude/skills/vercel-react-best-practices/SKILL.mdEsta skill funciona bien combinada con:
No apliques las 69 reglas de golpe. Empieza con CRÍTICO (Categorías 1-2) para el mayor ROI. Las reglas de menor prioridad importan menos hasta que se aborden las críticas.
React Compiler cambia las reglas de memoización. Si usas React Compiler, las reglas de la Categoría 5 (rerender-) sobre useMemo/useCallback manual pasan a ser menos relevantes. El compilador gestiona la mayoría automáticamente.
Algunas reglas entran en conflicto con la legibilidad. Reglas como js-combine-iterations (combinar filter+map en un solo bucle) pueden perjudicar la legibilidad por una ganancia mínima de rendimiento. Aplícalas solo en hot paths.
Las reglas de Server Component solo aplican a Next.js App Router. Si usas Pages Router o React plano, las reglas de la Categoría 3 (server-) no aplican.
| Alternativa | Úsala cuando | No la uses cuando |
|---|---|---|
| ESLint react-hooks/exhaustive-deps | Comprobaciones automatizadas en lint de deps de hooks | Necesites una revisión de rendimiento más amplia |
| @next/bundle-analyzer | Analizar el bundle real | Necesites comprobación de reglas a nivel de código |
| React DevTools Profiler | Perfilado en runtime de renderizados reales | Quieras análisis estático pre-commit |
| Lighthouse CI | Comprobaciones automatizadas de CWV en CI | Necesites recomendaciones a nivel de código |
// Mal: secuencial (waterfall)
const user = await getUser(id);
const posts = await getPosts(id);
// Bien: paralelo
const [user, posts] = await Promise.all([
getUser(id),
getPosts(id),
]);Promise.all() para operaciones async independientesimport { Button } from "@/components" arrastra todo el barrelimport { Button } from "@/components/ui/button" solo importa lo necesario// Mal: re-renderiza en cada cambio de count
const count = useStore((s) => s.count);
return <button onClick={() => console.log(count)}>Log</button>;
// Bien: lee el state solo dentro del manejador
return (
<button onClick={() => console.log(useStore.getState().count)}>
Log
</button>
);await auth() al inicio de cada Server Action// Arriesgado: renderiza "0" cuando count es 0
{count && <Badge count={count} />}
// Seguro: ternario explícito
{count > 0 ? <Badge count={count} /> : null}&& con un número puede renderizar 0 o false como texto visible// Mal: ejecuta el cómputo costoso en cada renderizado
const [data, setData] = useState(expensiveComputation());
// Bien: se ejecuta solo al montar
const [data, setData] = useState(() => expensiveComputation());useState para que el valor inicial costoso se calcule solo una vezuseMemo, useCallback y React.memo pasan a ser menos relevantes// Mal: pasa todo el objeto user al cliente
<ClientProfile user={user} />
// Bien: pasa solo los campos necesarios
<ClientProfile name={user.name} avatar={user.avatar} />await a la rama donde el valor se usa realmente// Mal: O(n) por comprobación
const isAdmin = adminIds.includes(userId);
// Bien: O(1) por comprobación
const adminSet = new Set(adminIds);
const isAdmin = adminSet.has(userId);Set para comprobaciones de pertenencia O(1) en lugar de Array.includes()Map para búsquedas repetidas en lugar de Array.find() en buclesRevisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥