[ PROMPT_NODE_27901 ]
React Performance Guidelines
[ SKILL_DOCUMENTATION ]
# React Best Practices - Complete Document
**Version 0.1.0**
Vercel Engineering
January 2026
> **Note:**
> This document is mainly for agents and LLMs to follow when maintaining, generating, or refactoring React and Next.js codebases at Vercel.
---
## Abstract
Comprehensive performance optimization guide for React and Next.js applications, designed for AI agents and LLMs. Contains 40+ rules across 8 categories, prioritized by impact from critical (eliminating waterfalls, reducing bundle size) to incremental (advanced patterns). Each rule includes detailed explanations, real-world examples comparing incorrect vs. correct implementations, and specific impact metrics to guide automated refactoring and code generation.
---
## Table of Contents
1. [Eliminating Waterfalls](#1-eliminating-waterfalls) — **CRITICAL**
- 1.1 [Defer Await Until Needed](#11)
- 1.2 [Dependency-Based Parallelization](#12)
- 1.3 [Prevent Waterfall Chains in API Routes](#13)
- 1.4 [Promise.all() for Independent Operations](#14)
- 1.5 [Strategic Suspense Boundaries](#15)
2. [Bundle Size Optimization](#2-bundle-size-optimization) — **CRITICAL**
- 2.1 [Avoid Barrel File Imports](#21)
- 2.2 [Conditional Module Loading](#22)
- 2.3 [Defer Non-Critical Third-Party Libraries](#23)
- 2.4 [Dynamic Imports for Heavy Components](#24)
- 2.5 [Preload Based on User Intent](#25)
3. [Server-Side Performance](#3-server-side-performance) — **HIGH**
- 3.1 [Cross-Request LRU Caching](#31)
- 3.2 [Minimize Serialization at RSC Boundaries](#32)
- 3.3 [Parallel Data Fetching with Component Composition](#33)
- 3.4 [Per-Request Deduplication with React.cache()](#34)
4. [Client-Side Data Fetching](#4-client-side-data-fetching) — **MEDIUM-HIGH**
- 4.1 [Deduplicate Global Event Listeners](#41)
- 4.2 [Use SWR for Automatic Deduplication](#42)
5. [Re-render Optimization](#5-re-render-optimization) — **MEDIUM**
- 5.1 [Defer State Reads to Usage Point](#51)
- 5.2 [Extract to Memoized Components](#52)
- 5.3 [Narrow Effect Dependencies](#53)
- 5.4 [Subscribe to Derived State](#54)
- 5.5 [Use Lazy State Initialization](#55)
- 5.6 [Use Transitions for Non-Urgent Updates](#56)
6. [Rendering Performance](#6-rendering-performance) — **MEDIUM**
- 6.1 [Animate SVG Wrapper Instead of SVG Element](#61)
- 6.2 [CSS content-visibility for Long Lists](#62)
- 6.3 [Hoist Static JSX Elements](#63)
- 6.4 [Optimize SVG Precision](#64)
- 6.5 [Prevent Hydration Mismatch Without Flickering](#65)
- 6.6 [Use Activity Component for Show/Hide](#66)
- 6.7 [Use Explicit Conditional Rendering](#67)
7. [JavaScript Performance](#7-javascript-performance) — **LOW-MEDIUM**
- 7.1 [Batch DOM CSS Changes](#71)
- 7.2 [Build Index Maps for Repeated Lookups](#72)
- 7.3 [Cache Property Access in Loops](#73)
- 7.4 [Cache Repeated Function Calls](#74)
- 7.5 [Cache Storage API Calls](#75)
- 7.6 [Combine Multiple Array Iterations](#76)
- 7.7 [Early Length Check for Array Comparisons](#77)
- 7.8 [Early Return from Functions](#78)
- 7.9 [Hoist RegExp Creation](#79)
- 7.10 [Use Loop for Min/Max Instead of Sort](#710)
- 7.11 [Use Set/Map for O(1) Lookups](#711)
- 7.12 [Use toSorted() Instead of sort() for Immutability](#712)
8. [Advanced Patterns](#8-advanced-patterns) — **LOW**
- 8.1 [Store Event Handlers in Refs](#81)
- 8.2 [useLatest for Stable Callback Refs](#82)
---
## 1. Eliminating Waterfalls
**Impact: CRITICAL**
Waterfalls are the #1 performance killer. Each sequential await adds full network latency. Eliminating them yields the largest gains.
### 1.1 Defer Await Until Needed
Move `await` operations into the branches where they're actually used to avoid blocking code paths that don't need them.
**Incorrect: blocks both branches**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
const userData = await fetchUserData(userId)
if (skipProcessing) {
// Returns immediately but still waited for userData
return { skipped: true }
}
// Only this branch uses userData
return processUserData(userData)
}
```
**Correct: only blocks when needed**
```typescript
async function handleRequest(userId: string, skipProcessing: boolean) {
if (skipProcessing) {
// Returns immediately without waiting
return { skipped: true }
}
// Fetch only when needed
const userData = await fetchUserData(userId)
return processUserData(userData)
}
```
**Another example: early return optimization**
```typescript
// Incorrect: always fetches permissions
async function updateResource(resourceId: string, userId: string) {
const permissions = await fetchPermissions(userId)
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
// Correct: fetches only when needed
async function updateResource(resourceId: string, userId: string) {
const resource = await getResource(resourceId)
if (!resource) {
return { error: 'Not found' }
}
const permissions = await fetchPermissions(userId)
if (!permissions.canEdit) {
return { error: 'Forbidden' }
}
return await updateResourceData(resource, permissions)
}
```
This optimization is especially valuable when the skipped branch is frequently taken, or when the deferred operation is expensive.
### 1.2 Dependency-Based Parallelization
For operations with partial dependencies, use `better-all` to maximize parallelism. It automatically starts each task at the earliest possible moment.
**Incorrect: profile waits for config unnecessarily**
```typescript
const [user, config] = await Promise.all([
fetchUser(),
fetchConfig()
])
const profile = await fetchProfile(user.id)
```
**Correct: config and profile run in parallel**
```typescript
import { all } from 'better-all'
const { user, config, profile } = await all({
async user() { return fetchUser() },
async config() { return fetchConfig() },
async profile() {
return fetchProfile((await this.$.user).id)
}
})
```
Reference: [https://github.com/shuding/better-all](https://github.com/shuding/better-all)
### 1.3 Prevent Waterfall Chains in API Routes
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
**Incorrect: config waits for auth, data waits for both**
```typescript
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 })
}
```
**Correct: auth and config start immediately**
```typescript
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 })
}
```
For operations with more complex dependency chains, use `better-all` to automatically maximize parallelism (see Dependency-Based Parallelization).
### 1.4 Promise.all() for Independent Operations
When async operations have no interdependencies, execute them concurrently using `Promise.all()`.
**Incorrect: sequential execution, 3 round trips**
```typescript
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
```
**Correct: parallel execution, 1 round trip**
```typescript
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(),
fetchComments()
])
```
### 1.5 Strategic Suspense Boundaries
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
**Incorrect: wrapper blocked by data fetching**
```tsx
async function Page() {
const data = await fetchData() // Blocks entire page
return (
)
}
```
The entire layout waits for data even though only the middle section needs it.
**Correct: wrapper shows immediately, data streams in**
```tsx
function Page() {
return (
)
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return
}
```
**Correct: both fetch simultaneously**
```tsx
async function Header() {
const data = await fetchHeader()
return
)
}
```
**Alternative with children prop:**
```tsx
async function Layout({ children }: { children: ReactNode }) {
const header = await fetchHeader()
return (
)
}
async function Sidebar() {
const items = await fetchSidebarItems()
return
}
export default function Page() {
return (
)
}
```
### 3.4 Per-Request Deduplication with React.cache()
Use `React.cache()` for server-side request deduplication. Authentication and database queries benefit most.
**Usage:**
```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 }
})
})
```
Within a single request, multiple calls to `getCurrentUser()` execute the query only once.
---
## 4. Client-Side Data Fetching
**Impact: MEDIUM-HIGH**
Automatic deduplication and efficient data fetching patterns reduce redundant network requests.
### 4.1 Deduplicate Global Event Listeners
Use `useSWRSubscription()` to share global event listeners across component instances.
**Incorrect: N instances = N listeners**
```tsx
function useKeyboardShortcut(key: string, callback: () => void) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && e.key === key) {
callback()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [key, callback])
}
```
When using the `useKeyboardShortcut` hook multiple times, each instance will register a new listener.
**Correct: N instances = 1 listener**
```tsx
import useSWRSubscription from 'swr/subscription'
// Module-level Map to track callbacks per key
const keyCallbacks = new Map<string, Set void>>()
function useKeyboardShortcut(key: string, callback: () => void) {
// Register this callback in the Map
useEffect(() => {
if (!keyCallbacks.has(key)) {
keyCallbacks.set(key, new Set())
}
keyCallbacks.get(key)!.add(callback)
return () => {
const set = keyCallbacks.get(key)
if (set) {
set.delete(callback)
if (set.size === 0) {
keyCallbacks.delete(key)
}
}
}
}, [key, callback])
useSWRSubscription('global-keydown', () => {
const handler = (e: KeyboardEvent) => {
if (e.metaKey && keyCallbacks.has(e.key)) {
keyCallbacks.get(e.key)!.forEach(cb => cb())
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}
}
function Profile() {
// Multiple shortcuts will share the same listener
useKeyboardShortcut('p', () => { /* ... */ })
useKeyboardShortcut('k', () => { /* ... */ })
// ...
}
```
### 4.2 Use SWR for Automatic Deduplication
SWR enables request deduplication, caching, and revalidation across component instances.
**Incorrect: no deduplication, each instance fetches**
```tsx
function UserList() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
}
```
**Correct: multiple instances share one request**
```tsx
import useSWR from 'swr'
function UserList() {
const { data: users } = useSWR('/api/users', fetcher)
}
```
**For immutable data:**
```tsx
import { useImmutableSWR } from '@/lib/swr'
function StaticContent() {
const { data } = useImmutableSWR('/api/config', fetcher)
}
```
**For mutations:**
```tsx
import { useSWRMutation } from 'swr/mutation'
function UpdateButton() {
const { trigger } = useSWRMutation('/api/user', updateUser)
return
}
```
Reference: [https://swr.vercel.app](https://swr.vercel.app)
---
## 5. Re-render Optimization
**Impact: MEDIUM**
Reducing unnecessary re-renders minimizes wasted computation and improves UI responsiveness.
### 5.1 Defer State Reads to Usage Point
Don't subscribe to dynamic state (searchParams, localStorage) if you only read it inside callbacks.
**Incorrect: subscribes to all searchParams changes**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const searchParams = useSearchParams()
const handleShare = () => {
const ref = searchParams.get('ref')
shareChat(chatId, { ref })
}
return
}
```
**Correct: reads on demand, no subscription**
```tsx
function ShareButton({ chatId }: { chatId: string }) {
const handleShare = () => {
const params = new URLSearchParams(window.location.search)
const ref = params.get('ref')
shareChat(chatId, { ref })
}
return
}
```
### 5.2 Extract to Memoized Components
Extract expensive work into memoized components to enable early returns before computation.
**Incorrect: computes avatar even when loading**
```tsx
function Profile({ user, loading }: Props) {
const avatar = useMemo(() => {
const id = computeAvatarId(user)
return
}, [user])
if (loading) return
return
)
}
```
### 5.3 Narrow Effect Dependencies
Specify primitive dependencies instead of objects to minimize effect re-runs.
**Incorrect: re-runs on any user field change**
```tsx
useEffect(() => {
console.log(user.id)
}, [user])
```
**Correct: re-runs only when id changes**
```tsx
useEffect(() => {
console.log(user.id)
}, [user.id])
```
**For derived state, compute outside effect:**
```tsx
// Incorrect: runs on width=767, 766, 765...
useEffect(() => {
if (width < 768) {
enableMobileMode()
}
}, [width])
// Correct: runs only on boolean transition
const isMobile = width {
if (isMobile) {
enableMobileMode()
}
}, [isMobile])
```
### 5.4 Subscribe to Derived State
Subscribe to derived boolean state instead of continuous values to reduce re-render frequency.
**Incorrect: re-renders on every pixel change**
```tsx
function Sidebar() {
const width = useWindowWidth() // updates continuously
const isMobile = width < 768
return
Sidebar
Header
Footer
Sidebar
Header
<Suspense fallback={}>
Footer
{data.content}
}
```
Sidebar, Header, and Footer render immediately. Only DataDisplay waits for data.
---
## 2. Bundle Size Optimization
**Impact: CRITICAL**
Reducing initial bundle size improves Time to Interactive and Largest Contentful Paint.
### 2.1 Avoid Barrel File Imports
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. **Barrel files** are entry points that re-export multiple modules (e.g., `index.js` that does `export * from './module'`).
Popular icon and component libraries can have **up to 10,000 re-exports** in their entry file. For many React packages, **it takes 200-800ms just to import them**, affecting both development speed and production cold starts.
**Incorrect: imports entire library**
```tsx
import { Check, X, Menu } from 'lucide-react'
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
// Loads 2,225 modules, takes ~4.2s extra in dev
```
**Correct: imports only what you need**
```tsx
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'
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
// Loads only what you use
```
**Alternative: Next.js 13.5+**
```js
// next.config.js - use optimizePackageImports
module.exports = {
experimental: {
optimizePackageImports: ['lucide-react', '@mui/material']
}
}
// Then you can keep the ergonomic barrel imports:
import { Check, X, Menu } from 'lucide-react'
// Automatically transformed to direct imports at build time
```
Direct imports provide 15-70% faster dev boot, 28% faster builds, 40% faster cold starts, and significantly faster HMR.
Libraries commonly affected: `lucide-react`, `@mui/material`, `@mui/icons-material`, `@tabler/icons-react`, `react-icons`, `@headlessui/react`, `@radix-ui/react-*`, `lodash`, `ramda`, `date-fns`, `rxjs`, `react-use`.
Reference: [https://vercel.com/blog/how-we-optimized-package-imports-in-next-js](https://vercel.com/blog/how-we-optimized-package-imports-in-next-js)
### 2.2 Conditional Module Loading
Load large data or modules only when a feature is activated.
**Example: lazy-load animation frames**
```tsx
function AnimationPlayer({ enabled }: { enabled: boolean }) {
const [frames, setFrames] = useState(null)
useEffect(() => {
if (enabled && !frames && typeof window !== 'undefined') {
import('./animation-frames.js')
.then(mod => setFrames(mod.frames))
.catch(() => setEnabled(false))
}
}, [enabled, frames])
if (!frames) return
return
}
```
The `typeof window !== 'undefined'` check prevents bundling this module for SSR, optimizing server bundle size and build speed.
### 2.3 Defer Non-Critical Third-Party Libraries
Analytics, logging, and error tracking don't block user interaction. Load them after hydration.
**Incorrect: blocks initial bundle**
```tsx
import { Analytics } from '@vercel/analytics/react'
export default function RootLayout({ children }) {
return (
{children}
)
}
```
**Correct: loads after hydration**
```tsx
import dynamic from 'next/dynamic'
const Analytics = dynamic(
() => import('@vercel/analytics/react').then(m => m.Analytics),
{ ssr: false }
)
export default function RootLayout({ children }) {
return (
{children}
)
}
```
### 2.4 Dynamic Imports for Heavy Components
Use `next/dynamic` to lazy-load large components not needed on initial render.
**Incorrect: Monaco bundles with main chunk ~300KB**
```tsx
import { MonacoEditor } from './monaco-editor'
function CodePanel({ code }: { code: string }) {
return
}
```
**Correct: Monaco loads on demand**
```tsx
import dynamic from 'next/dynamic'
const MonacoEditor = dynamic(
() => import('./monaco-editor').then(m => m.MonacoEditor),
{ ssr: false }
)
function CodePanel({ code }: { code: string }) {
return
}
```
### 2.5 Preload Based on User Intent
Preload heavy bundles before they're needed to reduce perceived latency.
**Example: preload on hover/focus**
```tsx
function EditorButton({ onClick }: { onClick: () => void }) {
const preload = () => {
if (typeof window !== 'undefined') {
void import('./monaco-editor')
}
}
return (
)
}
```
**Example: preload when feature flag is enabled**
```tsx
function FlagsProvider({ children, flags }: Props) {
useEffect(() => {
if (flags.editorEnabled && typeof window !== 'undefined') {
void import('./monaco-editor').then(mod => mod.init())
}
}, [flags.editorEnabled])
return
{children}
}
```
The `typeof window !== 'undefined'` check prevents bundling preloaded modules for SSR, optimizing server bundle size and build speed.
---
## 3. Server-Side Performance
**Impact: HIGH**
Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times.
### 3.1 Cross-Request LRU Caching
`React.cache()` only works within one request. For data shared across sequential requests (user clicks button A then button B), use an LRU cache.
**Implementation:**
```typescript
import { LRUCache } from 'lru-cache'
const cache = new LRUCache({
max: 1000,
ttl: 5 * 60 * 1000 // 5 minutes
})
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
}
// Request 1: DB query, result cached
// Request 2: cache hit, no DB query
```
Use when sequential user actions hit multiple endpoints needing the same data within seconds. In serverless, consider Redis for cross-process caching.
Reference: [https://github.com/isaacs/node-lru-cache](https://github.com/isaacs/node-lru-cache)
### 3.2 Minimize Serialization at RSC Boundaries
The React Server/Client boundary serializes all object properties. Only pass fields that the client actually uses.
**Incorrect: serializes all 50 fields**
```tsx
async function Page() {
const user = await fetchUser() // 50 fields
return
}
'use client'
function Profile({ user }: { user: User }) {
return {user.name}
// uses 1 field
}
```
**Correct: serializes only 1 field**
```tsx
async function Page() {
const user = await fetchUser()
return
}
'use client'
function Profile({ name }: { name: string }) {
return {name}
}
```
### 3.3 Parallel Data Fetching with Component Composition
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
**Incorrect: Sidebar waits for Page's fetch to complete**
```tsx
export default async function Page() {
const header = await fetchHeader()
return (
{header}
{data}
}
async function Sidebar() {
const items = await fetchSidebarItems()
return
}
export default function Page() {
return (
{header}
{children}
{avatar}
}
```
**Correct: skips computation when loading**
```tsx
const UserAvatar = memo(function UserAvatar({ user }: { user: User }) {
const id = useMemo(() => computeAvatarId(user), [user])
return
})
function Profile({ user, loading }: Props) {
if (loading) return
return (
Source: claude-code-templates (MIT). See About Us for full credits.
[ PARAMETER_INJECTION ]
SCANNING_VARIABLES...
[ INSTALL & COMMANDS ]
USAGE_GUIDE:
- Paste it into your AI terminal or config file.
[ SPONSORED_LINK ]