Claude
Skills
Sign in
Back

nextjs-rsc-performance-patterns

Included with Lifetime
$97 forever

Performance optimization patterns for Next.js 14+ React Server Components. PROACTIVELY activate for: (1) Optimizing RSC data fetching, (2) Implementing streaming with Suspense, (3) Configuring caching and revalidation, (4) Parallel data fetching, (5) Partial Prerendering. Triggers: "rsc", "server component", "nextjs performance", "data fetching", "caching", "revalidate", "suspense", "streaming", "partial prerendering"

Design

What this skill does


# Next.js RSC Performance Patterns

This skill provides expert knowledge on optimizing Next.js 14+ applications using the App Router, focusing on React Server Components (RSC), data fetching strategies, and rendering optimizations to improve Core Web Vitals.

## RSC Data Fetching

React Server Components enable you to fetch data directly on the server, eliminating client-server request waterfalls and reducing JavaScript bundle size.

### Parallel Data Fetching

Fetch independent data sources in parallel to minimize total wait time:

```tsx
// app/product/[id]/page.tsx - GOOD
async function ProductPage({ params }: { params: { id: string } }) {
  // These fetches happen in parallel - Next.js automatically deduplicates
  const productPromise = fetch(`https://api.example.com/products/${params.id}`)
  const reviewsPromise = fetch(`https://api.example.com/reviews?product=${params.id}`)

  // Wait for both to complete
  const [productRes, reviewsRes] = await Promise.all([productPromise, reviewsPromise])
  const product = await productRes.json()
  const reviews = await reviewsRes.json()

  return <ProductDetails product={product} reviews={reviews} />
}
```

### Sequential vs Parallel Pattern

**Sequential (when data depends on previous result):**
```tsx
async function UserDashboard({ userId }: { userId: string }) {
  const user = await fetchUser(userId)
  // This depends on user data, so must be sequential
  const preferences = await fetchPreferences(user.preferencesId)
  return <Dashboard user={user} preferences={preferences} />
}
```

**Parallel (when data is independent):**
```tsx
async function UserDashboard({ userId }: { userId: string }) {
  // Fetch independent data in parallel
  const [user, notifications, activity] = await Promise.all([
    fetchUser(userId),
    fetchNotifications(userId),
    fetchActivity(userId)
  ])
  return <Dashboard user={user} notifications={notifications} activity={activity} />
}
```

### Next.js Extended fetch API

Next.js extends the native fetch API with automatic request deduplication and granular caching control:

```tsx
// Cached for 1 hour (3600 seconds)
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }
})

// Revalidate on-demand using tags
const data = await fetch('https://api.example.com/products', {
  next: { tags: ['products'] }
})
// Later, trigger revalidation: revalidateTag('products')

// Force fresh data on every request (opt out of caching)
const data = await fetch('https://api.example.com/realtime', {
  cache: 'no-store'
})
```

## Streaming and Suspense

Streaming enables progressive loading, improving Time to First Byte (TTFB) and perceived performance by sending HTML to the browser incrementally.

### Basic Suspense Pattern

```tsx
// app/dashboard/page.tsx - GOOD
import { Suspense } from 'react'

async function RecentOrders() {
  // Slow data fetch
  const orders = await fetch('https://api.example.com/orders').then(r => r.json())
  return <OrdersList orders={orders} />
}

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Loading orders...</div>}>
        <RecentOrders />
      </Suspense>
    </div>
  )
}
```

The static content (h1) is sent immediately, while `RecentOrders` streams in when ready.

### Multiple Suspense Boundaries

```tsx
export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* These load independently and stream in as ready */}
      <Suspense fallback={<Skeleton />}>
        <RecentOrders />
      </Suspense>

      <Suspense fallback={<Skeleton />}>
        <Analytics />
      </Suspense>

      <Suspense fallback={<Skeleton />}>
        <Notifications />
      </Suspense>
    </div>
  )
}
```

### loading.js Convention

Next.js automatically wraps page content in a Suspense boundary when you provide a `loading.js` file:

```tsx
// app/dashboard/loading.tsx
export default function Loading() {
  return <div>Loading dashboard...</div>
}

// app/dashboard/page.tsx
export default async function Dashboard() {
  const data = await fetchDashboardData() // Suspense boundary is automatic
  return <DashboardView data={data} />
}
```

## Partial Prerendering (PPR)

Partial Prerendering (Next.js 14.1+) combines static and dynamic rendering within a single route, serving a static shell immediately while streaming dynamic content.

### Enable PPR

```js
// next.config.js
module.exports = {
  experimental: {
    ppr: true,
  },
}
```

### PPR Pattern

```tsx
// app/product/[id]/page.tsx
import { Suspense } from 'react'

export const experimental_ppr = true

async function Reviews({ productId }: { productId: string }) {
  // Dynamic content - will be streamed
  const reviews = await fetch(`https://api.example.com/reviews/${productId}`, {
    cache: 'no-store'
  }).then(r => r.json())

  return <ReviewList reviews={reviews} />
}

export default async function ProductPage({ params }: { params: { id: string } }) {
  // Static content - prerendered
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 3600 }
  }).then(r => r.json())

  return (
    <div>
      <ProductInfo product={product} />

      {/* Suspense boundary marks the dynamic "hole" */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={params.id} />
      </Suspense>
    </div>
  )
}
```

The static shell (product info) is instantly served, while reviews stream in dynamically.

## Caching and Revalidation

Next.js provides multiple caching layers: Request Memoization, Data Cache, Full Route Cache, and Router Cache.

### Time-Based Revalidation

```tsx
// Revalidate every hour
async function BlogPosts() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 }
  }).then(r => r.json())

  return <PostList posts={posts} />
}
```

### On-Demand Revalidation (Tag-Based)

```tsx
// app/products/page.tsx
async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] }
  }).then(r => r.json())

  return <ProductGrid products={products} />
}

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const tag = request.nextUrl.searchParams.get('tag')

  if (tag) {
    revalidateTag(tag) // Purges all fetches tagged with 'products'
    return NextResponse.json({ revalidated: true, now: Date.now() })
  }

  return NextResponse.json({ revalidated: false }, { status: 400 })
}
```

### On-Demand Revalidation (Path-Based)

```tsx
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const path = request.nextUrl.searchParams.get('path')

  if (path) {
    revalidatePath(path) // Purges cache for specific route
    return NextResponse.json({ revalidated: true })
  }

  return NextResponse.json({ revalidated: false }, { status: 400 })
}
```

### Caching Decision Tree

1. **Dynamic, real-time data** (stock prices, user sessions): `cache: 'no-store'`
2. **Frequently changing data** (social feeds, news): Short revalidate (60-300s)
3. **Periodically updated data** (blog posts, product catalogs): Medium revalidate (3600s) + on-demand revalidation
4. **Rarely changing data** (legal pages, documentation): Long revalidate (86400s) or static

## Anti-Patterns

### Client-Side Data Fetching in useEffect

```tsx
// BAD - Creates request waterfall
'use client'
import { useState, useEffect } from 'react'

export default function ProductInfo({ id }) {
  const [product, setProduct] = useState(null)

  useEffect(() => {
    // Fetch only starts after JS loads and executes
    fetch(`/api/products/${id}`)
      .then(r => r.json())
      .then(setProduct)
  }, [id])

  return product ? <Display da

Related in Design