React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

skillsvercelreactnextjsperformanceoptimization69-rules

Vercel React Best Practices Skill - An agent skill recipe for React and Next.js performance optimization

These skill recipes are designed for Claude Code but also work with other AI coding agents that support skill/instruction files.

Recipe

The complete SKILL.md content you can copy into .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.

Working Example

Example prompt: "Review this component for performance issues"

// 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>
  );
}

Example skill output:

The skill would identify these violations:

RuleSeverityIssue
server- (Category 3)CRITICALEntire page is "use client" with useEffect fetching. Should be a Server Component
async-parallelCRITICALSequential waterfall: user fetch then posts fetch
bundle-barrel-importsCRITICALImporting from barrel lucide-react pulls entire icon library
rerender-no-inline (Category 5)MEDIUMInline style objects recreated each render
rerender- (Category 5)MEDIUMArray index as key on dynamic list

And provide the refactored version:

// 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>
  );
}

Deep Dive

How It Works

  • The skill provides 69 rules organized by impact priority (CRITICAL to LOW)
  • Each rule has a prefix that categorizes it (async-, bundle-, server-, etc.)
  • When reviewing code, the skill checks against rules starting from the highest priority
  • When generating code, the skill follows Categories 1-3 by default
  • The rule-based approach ensures consistent, measurable performance improvements

Installation

mkdir -p .claude/skills/vercel-react-best-practices
# Copy the Recipe section above into:
# .claude/skills/vercel-react-best-practices/SKILL.md

Customization

  • Add project-specific rules to the bottom of the skill
  • Remove categories that do not apply (e.g., Category 8 for simple projects)
  • Adjust severity levels based on your performance targets
  • Add your bundle size budgets and Core Web Vitals thresholds

Pairing with Other Skills

This skill works well combined with:

  • react-19-mastery for React 19-specific patterns
  • nextjs-data-fetching for data layer decisions
  • typescript-react-patterns for type-safe implementations

Gotchas

  • Do not apply all 69 rules at once. Start with CRITICAL (Categories 1-2) for the highest ROI. Lower priority rules matter less until the critical ones are addressed.

  • React Compiler changes memoization rules. If using React Compiler, Category 5 (rerender-) rules around manual useMemo/useCallback become less relevant. The compiler handles most of these automatically.

  • Some rules conflict with readability. Rules like js-combine-iterations (combining filter+map into one loop) can hurt readability for minimal performance gain. Apply only on hot paths.

  • Server Component rules only apply to Next.js App Router. If using Pages Router or plain React, Category 3 (server-) rules do not apply.

Alternatives

AlternativeUse WhenDon't Use When
ESLint react-hooks/exhaustive-depsAutomated lint-time checks for hook depsNeed broader performance review
@next/bundle-analyzerAnalyzing actual bundle outputNeed code-level rule checking
React DevTools ProfilerRuntime profiling of real rendersWant pre-commit static analysis
Lighthouse CIAutomated CWV checks in CINeed code-level recommendations

FAQs

What are the two CRITICAL priority rule categories and why do they matter most?
  • Category 1: Eliminating Waterfalls -- sequential async operations are the biggest performance killer
  • Category 2: Bundle Size Optimization -- large bundles slow initial load and hurt Core Web Vitals
  • These two categories should be addressed before any other optimizations
How does the async-parallel rule prevent waterfalls?
// 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),
]);
  • Use Promise.all() for independent async operations
  • Never sequentially await promises that do not depend on each other
Why should you import directly from modules instead of barrel files?
  • Barrel (index) files prevent tree-shaking
  • import { Button } from "@/components" pulls in the entire barrel
  • import { Button } from "@/components/ui/button" only imports what is needed
What is the rerender-defer-reads rule and when does it apply?
// 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>
);
  • Do not subscribe to state that is only used inside event handlers
How should you use Suspense boundaries for streaming?
  • Each independent data-fetching section should have its own Suspense boundary
  • Avoid wrapping a slow and fast section in the same boundary (everything waits for the slowest)
  • Separate boundaries allow fast content to stream immediately
Gotcha: Why should every Server Action be authenticated?
  • Every Server Action is a public HTTP endpoint
  • An unauthenticated action allows anyone to call it directly
  • Always check await auth() at the start of every Server Action
What is the rendering-conditional-render rule about?
// Risky: renders "0" when count is 0
{count && <Badge count={count} />}
 
// Safe: explicit ternary
{count > 0 ? <Badge count={count} /> : null}
  • Using && with a number can render 0 or false as visible text
  • Always use a ternary or explicit boolean check
How does the rerender-lazy-state-init rule improve performance?
// Bad: runs expensive computation on every render
const [data, setData] = useState(expensiveComputation());
 
// Good: runs only on mount
const [data, setData] = useState(() => expensiveComputation());
  • Pass a function to useState so the expensive initial value is computed only once
Gotcha: Do Category 5 rerender rules still apply when using React Compiler?
  • The React Compiler handles most memoization automatically
  • Manual useMemo, useCallback, and React.memo rules become less relevant
  • However, structural rules (no inline components, lazy state init) still apply
What is the server-serialization rule and how do you follow it?
// Bad: passes entire user object to client
<ClientProfile user={user} />
 
// Good: passes only needed fields
<ClientProfile name={user.name} avatar={user.avatar} />
  • Minimize data passed from Server Components to Client Components
  • Only pass the specific fields the client component actually needs
How does the async-defer-await rule work?
  • Move await into the branch where the value is actually used
  • If some code paths do not need the async result, skip the await for those paths
  • This avoids unnecessary latency on branches that return early
What TypeScript pattern does the js-set-map-lookups rule recommend?
// 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);
  • Use Set for O(1) membership checks instead of Array.includes()
  • Use Map for repeated lookups instead of Array.find() in loops