Skip to content

Instantly share code, notes, and snippets.

@Rafael-Ramblas
Created March 2, 2026 17:01
Show Gist options
  • Select an option

  • Save Rafael-Ramblas/ab282f892c3c893846e5bb4d138e05b4 to your computer and use it in GitHub Desktop.

Select an option

Save Rafael-Ramblas/ab282f892c3c893846e5bb4d138e05b4 to your computer and use it in GitHub Desktop.
# This is a cursor rule generated from https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices
---
description: Vercel React & Next.js performance best practices - 57 rules across 8 categories
globs: "**/*.{ts,tsx}"
alwaysApply: false
---
# React Best Practices (Vercel Engineering)
Comprehensive performance optimization guide for React and Next.js applications. Apply these rules when writing, reviewing, or refactoring React/Next.js code.
---
## 1. Eliminating Waterfalls (CRITICAL)
Waterfalls are the #1 performance killer. Each sequential await adds full network latency.
### 1.1 Defer Await Until Needed
Move `await` into branches where actually used.
```typescript
// ❌ 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
```typescript
// ❌ BAD: sequential, 3 round trips
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
// ✅ GOOD: parallel, 1 round trip
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
```
### 1.3 Start Promises Early in API Routes
```typescript
// ❌ BAD: sequential
export async function GET(request: Request) {
const session = await auth()
const config = await fetchConfig()
const data = await fetchData(session.user.id)
return Response.json({ data, config })
}
// ✅ GOOD: parallel start
export async function GET(request: Request) {
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
const [config, data] = await Promise.all([
configPromise,
fetchData(session.user.id)
])
return Response.json({ data, config })
}
```
### 1.4 Strategic Suspense Boundaries
```tsx
// ❌ BAD: blocks entire page
async function Page() {
const data = await fetchData()
return <div><DataDisplay data={data} /></div>
}
// ✅ GOOD: streams content
function Page() {
return (
<div>
<Header />
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
<Footer />
</div>
)
}
```
---
## 2. Bundle Size Optimization (CRITICAL)
### 2.1 Avoid Barrel File Imports
```tsx
// ❌ BAD: loads entire library (200-800ms import cost)
import { Check, X, Menu } from 'lucide-react'
// ✅ GOOD: loads only what you need
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
// ✅ ALTERNATIVE: Next.js config
// next.config.js
module.exports = {
experimental: { optimizePackageImports: ['lucide-react', '@mui/material'] }
}
```
### 2.2 Dynamic Imports for Heavy Components
```tsx
// ❌ BAD: Monaco bundles with main chunk
import { MonacoEditor } from './monaco-editor'
// ✅ GOOD: 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 Third-Party Libraries
```tsx
// ❌ BAD: blocks initial bundle
import { Analytics } from '@vercel/analytics/react'
// ✅ GOOD: loads after hydration
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
```
### 2.4 Conditional Module Loading
```tsx
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
}
}, [enabled, frames])
```
### 2.5 Preload on User Intent
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') void import('./monaco-editor')
}
return <button onMouseEnter={preload} onFocus={preload} onClick={onClick}>Open</button>
}
```
---
## 3. Server-Side Performance (HIGH)
### 3.1 Authenticate Server Actions
```typescript
// ❌ BAD: no auth check
'use server'
export async function deleteUser(userId: string) {
await db.user.delete({ where: { id: userId } })
}
// ✅ GOOD: always verify auth
'use server'
export async function deleteUser(userId: string) {
const session = await verifySession()
if (!session || session.user.role !== 'admin') throw unauthorized()
await db.user.delete({ where: { id: userId } })
}
```
### 3.2 React.cache() for Per-Request Deduplication
```typescript
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.3 Minimize RSC Serialization
```tsx
// ❌ BAD: serializes all 50 fields
<Profile user={user} />
// ✅ GOOD: serializes only needed fields
<Profile name={user.name} avatar={user.avatar} />
```
### 3.4 Parallel Data Fetching with Composition
```tsx
// ❌ BAD: Sidebar waits for Page's fetch
export default async function Page() {
const header = await fetchHeader()
return <div><div>{header}</div><Sidebar /></div>
}
// ✅ GOOD: both fetch simultaneously
export default function Page() {
return <div><Header /><Sidebar /></div>
}
```
### 3.5 Use after() for Non-Blocking Operations
```typescript
import { after } from 'next/server'
export async function POST(request: Request) {
await updateDatabase(request)
after(async () => { logUserAction({ userAgent }) })
return Response.json({ status: 'success' })
}
```
### 3.6 LRU Cache for Cross-Request Caching
```typescript
import { LRUCache } from 'lru-cache'
const cache = new LRUCache<string, any>({ max: 1000, ttl: 5 * 60 * 1000 })
export async function getUser(id: string) {
const cached = cache.get(id)
if (cached) return cached
const user = await db.user.findUnique({ where: { id } })
cache.set(id, user)
return user
}
```
---
## 4. Client-Side Data Fetching (MEDIUM-HIGH)
### 4.1 Use SWR for Automatic Deduplication
```tsx
// ❌ BAD: no deduplication
const [users, setUsers] = useState([])
useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers) }, [])
// ✅ GOOD: multiple instances share one request
import useSWR from 'swr'
const { data: users } = useSWR('/api/users', fetcher)
```
### 4.2 Use Passive Event Listeners
```typescript
// ✅ GOOD: enables immediate scrolling
document.addEventListener('touchstart', handleTouch, { passive: true })
document.addEventListener('wheel', handleWheel, { passive: true })
```
### 4.3 Version localStorage Data
```typescript
const VERSION = 'v2'
function saveConfig(config: Config) {
try { localStorage.setItem(`config:${VERSION}`, JSON.stringify(config)) } catch {}
}
```
---
## 5. Re-render Optimization (MEDIUM)
### 5.1 Derive State During Render (Not in Effects)
```tsx
// ❌ BAD: redundant state and effect
const [fullName, setFullName] = useState('')
useEffect(() => { setFullName(firstName + ' ' + lastName) }, [firstName, lastName])
// ✅ GOOD: derive during render
const fullName = firstName + ' ' + lastName
```
### 5.2 Use Functional setState
```tsx
// ❌ BAD: stale closure risk
const addItem = useCallback((item) => setItems([...items, item]), [items])
// ✅ GOOD: stable callback, no stale closures
const addItem = useCallback((item) => setItems(curr => [...curr, item]), [])
```
### 5.3 Lazy State Initialization
```tsx
// ❌ BAD: runs on every render
const [settings, setSettings] = useState(JSON.parse(localStorage.getItem('settings') || '{}'))
// ✅ GOOD: runs only once
const [settings, setSettings] = useState(() => {
const stored = localStorage.getItem('settings')
return stored ? JSON.parse(stored) : {}
})
```
### 5.4 Extract to Memoized Components
```tsx
// ✅ GOOD: skips computation when loading
const UserAvatar = memo(function UserAvatar({ user }) {
const id = useMemo(() => computeAvatarId(user), [user])
return <Avatar id={id} />
})
function Profile({ user, loading }) {
if (loading) return <Skeleton />
return <UserAvatar user={user} />
}
```
### 5.5 Hoist Default Non-primitive Props
```tsx
// ❌ BAD: breaks memoization
const UserAvatar = memo(({ onClick = () => {} }) => { ... })
// ✅ GOOD: stable default
const NOOP = () => {}
const UserAvatar = memo(({ onClick = NOOP }) => { ... })
```
### 5.6 Use Primitive Dependencies in Effects
```tsx
// ❌ 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])
```
### 5.7 Put Interaction Logic in Event Handlers
```tsx
// ❌ BAD: effect re-runs on unrelated changes
useEffect(() => { if (submitted) post('/api/register') }, [submitted, theme])
// ✅ GOOD: do it in the handler
function handleSubmit() { post('/api/register'); showToast('Registered', theme) }
```
### 5.8 Use Transitions for Non-Urgent Updates
```tsx
import { startTransition } from 'react'
const handler = () => {
startTransition(() => setScrollY(window.scrollY))
}
```
### 5.9 Use useRef for Transient Values
```tsx
// ❌ BAD: renders every update
const [lastX, setLastX] = useState(0)
// ✅ GOOD: no re-render for tracking
const lastXRef = useRef(0)
useEffect(() => {
const onMove = (e) => { lastXRef.current = e.clientX }
window.addEventListener('mousemove', onMove)
return () => window.removeEventListener('mousemove', onMove)
}, [])
```
### 5.10 Don't Wrap Simple Expressions in useMemo
```tsx
// ❌ BAD: useMemo overhead exceeds benefit
const isLoading = useMemo(() => user.isLoading || notifications.isLoading, [user.isLoading, notifications.isLoading])
// ✅ GOOD: simple expression
const isLoading = user.isLoading || notifications.isLoading
```
### 5.11 Defer State Reads to Usage Point
```tsx
// ❌ BAD: subscribes to all searchParams changes
const searchParams = useSearchParams()
const handleShare = () => { shareChat(chatId, { ref: searchParams.get('ref') }) }
// ✅ GOOD: reads on demand
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
shareChat(chatId, { ref: params.get('ref') })
}
```
---
## 6. Rendering Performance (MEDIUM)
### 6.1 CSS content-visibility for Long Lists
```css
.message-item {
content-visibility: auto;
contain-intrinsic-size: 0 80px;
}
```
### 6.2 Hoist Static JSX
```tsx
// ✅ GOOD: reuses same element
const skeleton = <div className="animate-pulse h-20 bg-gray-200" />
function Container() { return <div>{loading && skeleton}</div> }
```
### 6.3 Animate SVG Wrapper (Not SVG Element)
```tsx
// ❌ BAD: no hardware acceleration
<svg className="animate-spin">...</svg>
// ✅ GOOD: hardware accelerated
<div className="animate-spin"><svg>...</svg></div>
```
### 6.4 Use Explicit Conditional Rendering
```tsx
// ❌ BAD: renders "0" when count is 0
{count && <span>{count}</span>}
// ✅ GOOD: renders nothing when count is 0
{count > 0 ? <span>{count}</span> : null}
```
### 6.5 Prevent Hydration Mismatch Without Flickering
Use inline script to set client-only values before React hydrates.
### 6.6 Use useTransition Over Manual Loading States
```tsx
const [isPending, startTransition] = useTransition()
const handleSearch = (value: string) => {
setQuery(value)
startTransition(async () => {
const data = await fetchResults(value)
setResults(data)
})
}
```
---
## 7. JavaScript Performance (LOW-MEDIUM)
### 7.1 Build Index Maps for Repeated Lookups
```typescript
// ❌ BAD: O(n) per lookup
orders.map(order => ({ ...order, user: users.find(u => u.id === order.userId) }))
// ✅ GOOD: O(1) per lookup
const userById = new Map(users.map(u => [u.id, u]))
orders.map(order => ({ ...order, user: userById.get(order.userId) }))
```
### 7.2 Use Set/Map for O(1) Lookups
```typescript
// ❌ BAD: O(n) per check
items.filter(item => allowedIds.includes(item.id))
// ✅ GOOD: O(1) per check
const allowedSet = new Set(allowedIds)
items.filter(item => allowedSet.has(item.id))
```
### 7.3 Combine Multiple Array Iterations
```typescript
// ❌ BAD: 3 iterations
const admins = users.filter(u => u.isAdmin)
const testers = users.filter(u => u.isTester)
// ✅ GOOD: 1 iteration
const admins: User[] = [], testers: User[] = []
for (const user of users) {
if (user.isAdmin) admins.push(user)
if (user.isTester) testers.push(user)
}
```
### 7.4 Early Length Check for Array Comparisons
```typescript
function hasChanges(current: string[], original: string[]) {
if (current.length !== original.length) return true
// ... expensive comparison only if lengths match
}
```
### 7.5 Use toSorted() for Immutability
```typescript
// ❌ BAD: mutates original
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.6 Avoid Layout Thrashing
```typescript
// ❌ BAD: interleaved reads and writes force reflows
element.style.width = '100px'
const width = element.offsetWidth // Forces reflow
element.style.height = '200px'
// ✅ GOOD: batch writes, then read
element.style.width = '100px'
element.style.height = '200px'
const { width, height } = element.getBoundingClientRect()
```
### 7.7 Hoist RegExp Creation
```tsx
// ❌ BAD: new RegExp every render
const regex = new RegExp(`(${query})`, 'gi')
// ✅ GOOD: memoize
const regex = useMemo(() => new RegExp(`(${escapeRegex(query)})`, 'gi'), [query])
```
### 7.8 Cache Property Access in Loops
```typescript
// ✅ GOOD
const value = obj.config.settings.value
const len = arr.length
for (let i = 0; i < len; i++) { process(value) }
```
### 7.9 Use Loop for Min/Max Instead of Sort
```typescript
// ❌ BAD: O(n log n)
const sorted = [...projects].sort((a, b) => b.updatedAt - a.updatedAt)
return sorted[0]
// ✅ GOOD: O(n)
let latest = projects[0]
for (const p of projects) if (p.updatedAt > latest.updatedAt) latest = p
return latest
```
---
## 8. Advanced Patterns (LOW)
### 8.1 Initialize App Once, Not Per Mount
```tsx
let didInit = false
function App() {
useEffect(() => {
if (didInit) return
didInit = true
loadFromStorage()
checkAuthToken()
}, [])
}
```
### 8.2 Store Event Handlers in Refs / useEffectEvent
```tsx
import { useEffectEvent } from 'react'
function useWindowEvent(event: string, handler: (e) => void) {
const onEvent = useEffectEvent(handler)
useEffect(() => {
window.addEventListener(event, onEvent)
return () => window.removeEventListener(event, onEvent)
}, [event])
}
```
---
## References
- [react.dev](https://react.dev)
- [nextjs.org](https://nextjs.org)
- [swr.vercel.app](https://swr.vercel.app)
- [Vercel Blog: Package Import Optimization](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
- [Vercel Blog: Dashboard Performance](https://vercel.com/blog/how-we-made-the-vercel-dashboard-twice-as-fast)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment