Skill de mejores prácticas de React de Vercel - Una receta de skill para optimización del rendimiento de React y Next.js
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.
Receta
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.Ejemplo funcional
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>
);
}Profundización
Cómo funciona
- La skill proporciona 69 reglas organizadas por prioridad de impacto (CRÍTICO a BAJO)
- Cada regla tiene un prefijo que la categoriza (async-, bundle-, server-, etc.)
- Al revisar código, la skill comprueba las reglas empezando por la mayor prioridad
- Al generar código, la skill sigue las Categorías 1-3 por defecto
- El enfoque basado en reglas garantiza mejoras de rendimiento consistentes y medibles
Instalación
mkdir -p .claude/skills/vercel-react-best-practices
# Copia la sección Receta de arriba en:
# .claude/skills/vercel-react-best-practices/SKILL.mdPersonalización
- Añade reglas específicas del proyecto al final de la skill
- Elimina categorías que no apliquen (p. ej., Categoría 8 para proyectos simples)
- Ajusta los niveles de severidad según tus objetivos de rendimiento
- Añade tus presupuestos de tamaño de bundle y umbrales de Core Web Vitals
Combinación con otras skills
Esta skill funciona bien combinada con:
- react-19-mastery para patrones específicos de React 19
- nextjs-data-fetching para decisiones de la capa de datos
- typescript-react-patterns para implementaciones con tipos seguros
Errores comunes
-
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.
Alternativas
| 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 |
Preguntas frecuentes
¿Cuáles son las dos categorías de reglas con prioridad CRÍTICA y por qué importan más?
- Categoría 1: Eliminar waterfalls — las operaciones async secuenciales son el mayor asesino del rendimiento
- Categoría 2: Optimización del tamaño del bundle — los bundles grandes ralentizan la carga inicial y perjudican Core Web Vitals
- Estas dos categorías deben abordarse antes que cualquier otra optimización
¿Cómo evita la regla async-parallel los waterfalls?
// 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),
]);- Usa
Promise.all()para operaciones async independientes - Nunca hagas await secuencial de promesas que no dependan unas de otras
¿Por qué deberías importar directamente desde módulos en lugar de archivos barrel?
- Los archivos barrel (index) impiden el tree-shaking
import { Button } from "@/components"arrastra todo el barrelimport { Button } from "@/components/ui/button"solo importa lo necesario
¿Qué es la regla rerender-defer-reads y cuándo aplica?
// 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>
);- No te suscribas a state que solo se usa dentro de manejadores de eventos
¿Cómo deberías usar límites Suspense para streaming?
- Cada sección independiente de obtención de datos debe tener su propio límite Suspense
- Evita envolver una sección lenta y una rápida en el mismo límite (todo espera a la más lenta)
- Límites separados permiten que el contenido rápido se transmita de inmediato
Trampa: ¿Por qué debería autenticarse cada Server Action?
- Cada Server Action es un endpoint HTTP público
- Una action sin autenticar permite que cualquiera la invoque directamente
- Comprueba siempre
await auth()al inicio de cada Server Action
¿De qué trata la regla rendering-conditional-render?
// Arriesgado: renderiza "0" cuando count es 0
{count && <Badge count={count} />}
// Seguro: ternario explícito
{count > 0 ? <Badge count={count} /> : null}- Usar
&&con un número puede renderizar0ofalsecomo texto visible - Usa siempre un ternario o una comprobación booleana explícita
¿Cómo mejora el rendimiento la regla rerender-lazy-state-init?
// 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());- Pasa una función a
useStatepara que el valor inicial costoso se calcule solo una vez
Trampa: ¿Siguen aplicando las reglas rerender de la Categoría 5 al usar React Compiler?
- React Compiler gestiona la mayoría de la memoización automáticamente
- Las reglas manuales de
useMemo,useCallbackyReact.memopasan a ser menos relevantes - Sin embargo, las reglas estructurales (sin componentes inline, lazy state init) siguen aplicando
¿Qué es la regla server-serialization y cómo seguirla?
// Mal: pasa todo el objeto user al cliente
<ClientProfile user={user} />
// Bien: pasa solo los campos necesarios
<ClientProfile name={user.name} avatar={user.avatar} />- Minimiza los datos pasados de Server Components a Client Components
- Pasa solo los campos específicos que el componente cliente realmente necesita
¿Cómo funciona la regla async-defer-await?
- Mueve el
awaita la rama donde el valor se usa realmente - Si algunas rutas de código no necesitan el resultado async, omite el await en esas rutas
- Esto evita latencia innecesaria en ramas que retornan temprano
¿Qué patrón de TypeScript recomienda la regla js-set-map-lookups?
// 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);- Usa
Setpara comprobaciones de pertenencia O(1) en lugar deArray.includes() - Usa
Mappara búsquedas repetidas en lugar deArray.find()en bucles
Relacionado
- Re-renderizados - prevenir re-renderizados innecesarios
- Optimización del bundle - reducir el tamaño del bundle
- Core Web Vitals - optimización de CWV
- Lista de comprobación de rendimiento - lista de auditoría
- Rendimiento de obtención de datos - eliminar waterfalls