vercel-react-best-practices

Official React and Next.js performance optimization guidelines from Vercel Engineering. Auto-activates when writing, reviewing, or refactoring React components, Next.js pages, data fetching, bundle op

SKILL.md

GitHub

Vercel React Best Practices

Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel Engineering. Contains rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.

Rule Categories by Priority

Priority Category Impact Prefix Focus
1 Eliminating Waterfalls CRITICAL async- Parallel fetches, deferred awaits, Suspense streaming
2 Bundle Size Optimization CRITICAL bundle- Direct imports over barrel files, dynamic imports, conditional loading
3 Server-Side Performance HIGH server- React.cache(), non-blocking after(), data minimization
4 Client-Side Data Fetching MEDIUM-HIGH client- SWR / TanStack Query, optimistic UI, caching
5 Re-render Optimization MEDIUM rerender- Reference stability, context splitting, derived state
6 Rendering Performance MEDIUM rendering- Virtualization, content-visibility, memoization
7 JavaScript Performance LOW-MEDIUM js- Algorithmic efficiency, avoiding work in render
8 Advanced Patterns LOW advanced- Islands, partial hydration, micro-optimizations

1. Eliminating Waterfalls (CRITICAL)

async-defer-await

Move await into the exact branches where the value is needed, not at the top of functions:

// ❌ WRONG: Blocks execution even if user branch is never hit
async function handleRequest(userId?: string) {
  const user = userId ? await fetchUser(userId) : null
  if (!user) return redirect('/login')
  return renderProfile(user)
}

// ✅ CORRECT: Start early or await inside condition
async function handleRequest(userId?: string) {
  if (!userId) return redirect('/login')
  const user = await fetchUser(userId)
  return renderProfile(user)
}

async-parallel

Use Promise.all() for independent asynchronous operations:

// ❌ WRONG: Sequential waterfall (takes T1 + T2)
const user = await fetchUser(id)
const posts = await fetchPosts(id)

// ✅ CORRECT: Concurrent fetches (takes max(T1, T2))
const [user, posts] = await Promise.all([
  fetchUser(id),
  fetchPosts(id),
])

async-suspense-boundaries

Stream slow components independently using React Suspense rather than awaiting data in parent layouts:

// ✅ Stream heavy content without blocking the main shell
export default function Page() {
  return (
    <div>
      <Header />
      <Suspense fallback={<AnalyticsSkeleton />}>
        <SlowAnalyticsDashboard />
      </Suspense>
    </div>
  )
}

2. Bundle Size Optimization (CRITICAL)

bundle-barrel-imports

Import directly from the module source; avoid large barrel files (index.ts re-exporting hundreds of icons or utilities), as barrel files defeat compiler tree-shaking and bloat JS bundles:

// ❌ WRONG: Imports entire icon library barrel file
import { ChevronRight, Settings } from 'lucide-react'

// ✅ PREFERRED in large bundles: Direct or optimized path
import ChevronRight from 'lucide-react/dist/esm/icons/chevron-right'

bundle-dynamic-imports

Dynamically import heavy components (charts, rich-text editors, maps) with next/dynamic so they are not included in the initial page chunk:

import dynamic from 'next/dynamic'

const HeavyChart = dynamic(() => import('@/components/AnalyticsChart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // only if chart requires window / client canvas
})

bundle-defer-third-party

Load non-critical analytics, customer support widgets, and logging scripts only after hydration using next/script with strategy="afterInteractive" or strategy="lazyOnload".


3. Server-Side Performance & Next.js 15 (HIGH)

Next.js 15 Default Caching Behavior

[!IMPORTANT] In Next.js 15, fetch requests and GET Route Handlers are uncached by default (cache: 'no-store'). If you want data to be cached, you must explicitly declare cache: 'force-cache' or next: { revalidate: 3600 }.

// ✅ Explicit caching when data is cacheable
const res = await fetch('https://api.example.com/products', {
  next: { revalidate: 60 }, // ISR: cached for 60s
})

server-cache-react

Use React.cache() for per-request function deduplication across Server Components:

import { cache } from 'react'

export const getOrganization = cache(async (orgId: string) => {
  return await db.organization.findUnique({ where: { id: orgId } })
})

server-after-nonblocking

In Next.js 15+, use after() from next/server to run non-critical side effects (analytics logging, cache warming, notifications) without delaying the HTTP response:

import { after } from 'next/server'

export async function POST(request: Request) {
  const data = await request.json()
  const result = await processOrder(data)

  // Runs in background after response is sent to client
  after(async () => {
    await sendAuditLog(result.id)
    await notifySlack(result)
  })

  return Response.json({ success: true, id: result.id })
}

4. Client-Side & React 19 Patterns

useActionState & useOptimistic (React 19)

Replace legacy useTransition + manual error states with React 19 Server Action hooks:

'use client'
import { useActionState, useOptimistic } from 'react'
import { updateNameAction } from '@/actions/profile'

export function ProfileForm({ initialName }: { initialName: string }) {
  const [state, formAction, isPending] = useActionState(updateNameAction, null)
  const [optimisticName, setOptimisticName] = useOptimistic(
    initialName,
    (_current, update: string) => update
  )

  return (
    <form action={async (formData) => {
      setOptimisticName(formData.get('name') as string)
      await formAction(formData)
    }}>
      <input name="name" defaultValue={optimisticName} />
      <button disabled={isPending}>Save</button>
    </form>
  )
}

No forwardRef in React 19

React 19 allows passing ref directly as a component prop. Do not wrap modern components in forwardRef().