Agent skill
react-best-practices
React and Next.js performance optimization patterns. Use BEFORE implementing any React code to ensure best practices are followed.
Install this agent skill to your Project
npx add-skill https://github.com/AvivK5498/The-Claude-Protocol/tree/main/templates/skills/react-best-practices
SKILL.md
React Best Practices
Version 1.0.0 Source: Vercel Engineering (vercel-labs/agent-skills)
Note: This document is for agents and LLMs to follow when maintaining, generating, or refactoring React and Next.js codebases. Contains 40+ rules across 8 categories, prioritized by impact.
How to Use This Skill
Before implementing ANY React/Next.js code:
- Review the relevant sections based on what you're building
- Apply the patterns as you write code
- Use the "Incorrect" vs "Correct" examples as templates
Priority order: Eliminating Waterfalls > Bundle Size > Server-Side > Client-Side > Re-renders > Rendering > JS Perf > Advanced
Quick Reference: Critical Rules
Top 5 Rules (Always Apply)
- Promise.all() for independent operations - Never sequential awaits for independent data
- Avoid barrel file imports - Import directly from source files
- Dynamic imports for heavy components - Lazy-load Monaco, charts, etc.
- Parallel data fetching with component composition - Structure RSC for parallelism
- Minimize serialization at RSC boundaries - Only pass needed fields to client
1. Eliminating Waterfalls
Impact: CRITICAL - Waterfalls are the #1 performance killer.
1.1 Defer Await Until Needed
Move await into branches where actually used.
// BAD: blocks both branches
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) return { skipped: true }
return processUserData(userData)
}
// GOOD: only blocks when needed
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) return { skipped: true }
const userData = await fetchUserData(userId)
return processUserData(userData)
}
1.2 Promise.all() for Independent Operations
// BAD: 3 round trips
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
// GOOD: 1 round trip
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
1.3 Strategic Suspense Boundaries
// BAD: wrapper blocked by data
async function Page() {
const data = await fetchData()
return (
<div>
<Sidebar />
<DataDisplay data={data} />
<Footer />
</div>
)
}
// GOOD: wrapper shows immediately
function Page() {
return (
<div>
<Sidebar />
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
<Footer />
</div>
)
}
2. Bundle Size Optimization
Impact: CRITICAL - Reduces TTI and LCP.
2.1 Avoid Barrel File Imports
// BAD: loads 1,583 modules
import { Check, X, Menu } from 'lucide-react'
// GOOD: loads only 3 modules
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
// ALTERNATIVE: Next.js 13.5+ config
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
2.2 Dynamic Imports for Heavy Components
// BAD: Monaco bundles with main chunk (~300KB)
import { MonacoEditor } from './monaco-editor'
// GOOD: Monaco loads on demand
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
2.3 Defer Non-Critical Libraries
// BAD: blocks initial bundle
import { Analytics } from '@vercel/analytics/react'
// GOOD: loads after hydration
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
2.4 Preload on User Intent
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
<button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
Open Editor
</button>
)
}
3. Server-Side Performance
Impact: HIGH
3.1 Minimize Serialization at RSC Boundaries
// BAD: serializes all 50 fields
async function Page() {
const user = await fetchUser() // 50 fields
return <Profile user={user} />
}
// GOOD: serializes only needed fields
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} avatar={user.avatar} />
}
3.2 Parallel Data Fetching with Component Composition
// BAD: Sidebar waits for Header's fetch
export default async function Page() {
const header = await fetchHeader()
return (
<div>
<div>{header}</div>
<Sidebar />
</div>
)
}
// GOOD: both fetch simultaneously
async function Header() {
const data = await fetchHeader()
return <div>{data}</div>
}
async function Sidebar() {
const items = await fetchSidebarItems()
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<div>
<Header />
<Sidebar />
</div>
)
}
3.3 Per-Request Deduplication with React.cache()
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({ where: { id: session.user.id } })
})
3.4 Use after() for Non-Blocking Operations
import { after } from 'next/server'
export async function POST(request: Request) {
await updateDatabase(request)
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent')
logUserAction({ userAgent })
})
return Response.json({ status: 'success' })
}
4. Client-Side Data Fetching
Impact: MEDIUM-HIGH
4.1 Use SWR for Automatic Deduplication
// BAD: no deduplication
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users').then(r => r.json()).then(setUsers)
}, [])
}
// GOOD: multiple instances share one request
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
5. Re-render Optimization
Impact: MEDIUM
5.1 Use Functional setState Updates
// BAD: requires state as dependency, risk of stale closure
const addItems = useCallback((newItems: Item[]) => {
setItems([...items, ...newItems])
}, [items])
// GOOD: stable callback, no stale closures
const addItems = useCallback((newItems: Item[]) => {
setItems(curr => [...curr, ...newItems])
}, [])
5.2 Use Lazy State Initialization
// BAD: runs on every render
const [settings] = useState(JSON.parse(localStorage.getItem('settings') || '{}'))
// GOOD: runs only once
const [settings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
5.3 Use Transitions for Non-Urgent Updates
import { startTransition } from 'react'
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0)
useEffect(() => {
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
window.addEventListener('scroll', handler, { passive: true })
return () => window.removeEventListener('scroll', handler)
}, [])
}
5.4 Narrow Effect Dependencies
// BAD: re-runs on any user field change
useEffect(() => {
console.log(user.id)
}, [user])
// GOOD: re-runs only when id changes
useEffect(() => {
console.log(user.id)
}, [user.id])
6. Rendering Performance
Impact: MEDIUM
6.1 CSS content-visibility for Long Lists
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
6.2 Hoist Static JSX Elements
// BAD: recreates element every render
function Container() {
return loading && <div className="animate-pulse h-20 bg-gray-200" />
}
// GOOD: reuses same element
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />
function Container() {
return loading && loadingSkeleton
}
6.3 Animate SVG Wrapper, Not SVG Element
// BAD: no hardware acceleration
<svg className="animate-spin">...</svg>
// GOOD: hardware accelerated
<div className="animate-spin">
<svg>...</svg>
</div>
7. JavaScript Performance
Impact: LOW-MEDIUM
7.1 Build Index Maps for Repeated Lookups
// BAD: O(n) per lookup
items.filter(item => allowedIds.includes(item.id))
// GOOD: O(1) per lookup
const allowedSet = new Set(allowedIds)
items.filter(item => allowedSet.has(item.id))
7.2 Use toSorted() Instead of sort()
// BAD: mutates original array
const sorted = users.sort((a, b) => a.name.localeCompare(b.name))
// GOOD: creates new array
const sorted = users.toSorted((a, b) => a.name.localeCompare(b.name))
7.3 Early Return from Functions
// BAD: processes all items after finding error
function validateUsers(users: User[]) {
let hasError = false
for (const user of users) {
if (!user.email) hasError = true
}
return hasError ? { valid: false } : { valid: true }
}
// GOOD: returns immediately on first error
function validateUsers(users: User[]) {
for (const user of users) {
if (!user.email) return { valid: false, error: 'Email required' }
}
return { valid: true }
}
8. Advanced Patterns
Impact: LOW
8.1 useEffectEvent for Stable Callbacks
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: () => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
Checklist Before Implementation
- Independent async operations use Promise.all()
- Heavy components use dynamic imports
- RSC boundaries pass only needed fields
- Suspense boundaries isolate data fetching
- No barrel file imports for large libraries
- State updates use functional form when depending on current state
- Effects have narrow dependencies
- Repeated lookups use Set/Map
References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
subagents-discipline
Core engineering principles for implementation tasks
create-beads-orchestration
Bootstrap lean multi-agent orchestration with beads task tracking. Use for projects needing agent delegation without heavy MCP overhead.
subagents-discipline
Invoke at the start of any implementation task to enforce verification-first development
create-beads-orchestration
Bootstrap lean multi-agent orchestration with beads task tracking. Use for projects needing agent delegation without heavy MCP overhead.
agentifind-benchmark
Create a benchmark to measure CODEBASE.md effectiveness. Sets up hooks to run two parallel agents (one with guide, one without) and compare their efficiency. Requires /agentifind to be run first.
edit-article
Edit and improve articles by restructuring sections, improving clarity, and tightening prose. Use when user wants to edit, revise, or improve an article draft.
Didn't find tool you were looking for?