first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
{
"name": "nextjs",
"owner": {
"name": "Vercel",
"url": "https://vercel.com"
},
"plugins": [
{
"name": "cache-components",
"source": "./plugins/cache-components",
"description": "Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). Proactively activates in projects with cacheComponents enabled.",
"version": "1.0.0",
"author": {
"name": "Next.js Team"
}
}
]
}

View File

@@ -0,0 +1,128 @@
# Next.js Claude Code Plugins
This directory contains Claude Code plugins for Next.js development.
## Using the Next.js Plugin Marketplace
The Next.js repository serves as a Claude Code plugin marketplace. Team members and contributors can install plugins directly from this repo.
### Quick Start
```bash
# Add the Next.js marketplace
/plugin marketplace add vercel/next.js
# List available plugins
/plugin list
# Install a plugin
/plugin install cache-components@nextjs
```
### Available Plugins
| Plugin | Description |
|--------|-------------|
| `cache-components` | Expert guidance for Cache Components and PPR |
## For Team Members
To auto-enable plugins for everyone working in a Next.js project, add to `.claude/settings.json`:
```json
{
"extraKnownMarketplaces": {
"nextjs": {
"source": {
"source": "github",
"repo": "vercel/next.js"
}
}
},
"enabledPlugins": {
"cache-components@nextjs": true
}
}
```
## Creating New Plugins
To add a new plugin to the marketplace:
### 1. Create Plugin Directory
```bash
mkdir -p .claude-plugin/plugins/my-plugin/.claude-plugin
mkdir -p .claude-plugin/plugins/my-plugin/skills/my-skill
```
### 2. Create Plugin Manifest
**File**: `.claude-plugin/plugins/my-plugin/.claude-plugin/plugin.json`
```json
{
"name": "my-plugin",
"version": "1.0.0",
"description": "What the plugin does",
"author": {
"name": "Next.js Team"
}
}
```
### 3. Create Skill
**File**: `.claude-plugin/plugins/my-plugin/skills/my-skill/SKILL.md`
```yaml
---
name: my-skill
description: When to use this skill
---
# My Skill
Instructions for Claude...
```
### 4. Register in Marketplace
Add to `.claude-plugin/marketplace.json`:
```json
{
"plugins": [
{
"name": "my-plugin",
"source": "./plugins/my-plugin",
"description": "What it does"
}
]
}
```
### 5. Test Locally
```bash
claude --plugin-dir .claude-plugin/plugins/my-plugin
```
## Plugin Structure
```
.claude-plugin/
├── marketplace.json ← Marketplace catalog
└── plugins/
├── README.md ← This file
└── cache-components/
├── .claude-plugin/
│ └── plugin.json ← Plugin manifest
├── skills/
│ └── cache-components/
│ ├── SKILL.md ← Main skill file
│ ├── REFERENCE.md ← API reference
│ ├── PATTERNS.md ← Usage patterns
│ └── TROUBLESHOOTING.md ← Debugging guide
└── README.md ← Plugin documentation
```

View File

@@ -0,0 +1,20 @@
{
"name": "cache-components",
"version": "1.0.0",
"description": "Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). Proactively activates in projects with cacheComponents: true, providing patterns for 'use cache' directive, cacheLife(), cacheTag(), cache invalidation, and parameter permutation rendering.",
"author": {
"name": "Next.js Team",
"url": "https://nextjs.org"
},
"homepage": "https://nextjs.org/docs/app/building-your-application/caching",
"repository": "https://github.com/vercel/next.js",
"license": "MIT",
"keywords": [
"nextjs",
"cache",
"ppr",
"partial-prerendering",
"react-server-components",
"use-cache"
]
}

View File

@@ -0,0 +1,112 @@
# Cache Components Plugin for Claude Code
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR).
## Features
This plugin provides a comprehensive skill that:
- **Proactively activates** in projects with `cacheComponents: true`
- Teaches the `'use cache'` directive, `cacheLife()`, `cacheTag()`, and invalidation APIs
- Explains **parameter permutation rendering** and subshell generation
- Covers migration from deprecated `revalidate`/`dynamic` segment configs
- Provides build-time error solutions and debugging guidance
## Installation
### Step 1: Add the Next.js Marketplace
```
/plugin marketplace add vercel/next.js
```
### Step 2: Install the Plugin
```
/plugin install cache-components@nextjs
```
Or install via CLI:
```bash
claude plugin install cache-components@nextjs
```
### Step 3 (Optional): Enable for Your Team
Add to your project's `.claude/settings.json` to auto-enable for all team members:
```json
{
"enabledPlugins": {
"cache-components@nextjs": true
}
}
```
## What's Included
| File | Description |
|------|-------------|
| `SKILL.md` | Core concepts, APIs, and proactive application guidelines |
| `REFERENCE.md` | Complete API reference, generateStaticParams, deprecated configs |
| `PATTERNS.md` | 12 production patterns including subshell composition |
| `TROUBLESHOOTING.md` | Build errors, debugging techniques, common issues |
## Usage
Once installed, the skill automatically activates when:
1. You're working in a Next.js project with `cacheComponents: true`
2. You ask about caching, PPR, or the `'use cache'` directive
3. You're writing React Server Components or Server Actions
### Example Triggers
- "How do I cache this data fetching function?"
- "What's the difference between updateTag and revalidateTag?"
- "I'm getting a build error about uncached data outside Suspense"
- "Help me set up generateStaticParams for my product pages"
## Key Concepts Covered
### Parameter Permutation Rendering
When you provide `generateStaticParams`, Next.js renders ALL permutations:
```
generateStaticParams returns:
[{ category: 'jackets', slug: 'bomber' }]
Next.js renders:
/products/jackets/bomber ← Complete page
/products/jackets/[slug] ← Category subshell (reusable!)
/products/[category]/[slug] ← Fallback shell
```
### Deprecated Segment Configs
| Old (Deprecated) | New (Cache Components) |
|------------------|------------------------|
| `export const revalidate = 3600` | `cacheLife('hours')` inside `'use cache'` |
| `export const dynamic = 'force-static'` | Use `'use cache'` + Suspense |
## Contributing
This plugin lives in the Next.js repository at `.claude-plugin/plugins/cache-components/`.
To contribute improvements:
1. Edit files in `.claude-plugin/plugins/cache-components/skills/cache-components/`
2. Test locally with `claude --plugin-dir .claude-plugin/plugins/cache-components`
3. Submit a PR to the Next.js repository
## Version History
### 1.0.0
- Initial release
- Covers `'use cache'`, `cacheLife()`, `cacheTag()`, `updateTag()`, `revalidateTag()`
- Parameter permutation rendering and subshell generation
- Migration guide from deprecated segment configs
- Build-time feedback and troubleshooting

View File

@@ -0,0 +1,832 @@
# Cache Components Patterns & Recipes
Common patterns for implementing Cache Components effectively.
## Pattern 1: Static + Cached + Dynamic Page
The foundational pattern for Partial Prerendering:
```tsx
import { Suspense } from 'react'
import { cacheLife } from 'next/cache'
// Static - no special handling needed
function Header() {
return <header>My Blog</header>
}
// Cached - included in static shell
async function FeaturedPosts() {
'use cache'
cacheLife('hours')
const posts = await db.posts.findMany({
where: { featured: true },
take: 5,
})
return (
<section>
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</section>
)
}
// Dynamic - streams at request time
async function PersonalizedFeed() {
const session = await getSession()
const feed = await db.posts.findMany({
where: { authorId: { in: session.following } },
})
return <FeedList posts={feed} />
}
// Page composition
export default async function HomePage() {
return (
<>
<Header />
<FeaturedPosts />
<Suspense fallback={<FeedSkeleton />}>
<PersonalizedFeed />
</Suspense>
</>
)
}
```
---
## Pattern 2: Read-Your-Own-Writes with Server Actions
Ensure users see their changes immediately:
```tsx
// components/posts.tsx
import { cacheTag, cacheLife } from 'next/cache'
async function PostsList() {
'use cache'
cacheTag('posts')
cacheLife('hours')
const posts = await db.posts.findMany({ orderBy: { createdAt: 'desc' } })
return (
<ul>
{posts.map((p) => (
<PostItem key={p.id} post={p} />
))}
</ul>
)
}
// actions/posts.ts
'use server'
import { updateTag } from 'next/cache'
export async function createPost(formData: FormData) {
const post = await db.posts.create({
data: {
title: formData.get('title') as string,
content: formData.get('content') as string,
},
})
// Immediate invalidation - user sees new post right away
updateTag('posts')
return { success: true, postId: post.id }
}
// components/create-post-form.tsx
'use client'
import { useTransition } from 'react'
import { createPost } from '@/actions/posts'
export function CreatePostForm() {
const [isPending, startTransition] = useTransition()
return (
<form
action={(formData) => {
startTransition(() => createPost(formData))
}}
>
<input name="title" required />
<textarea name="content" required />
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create Post'}
</button>
</form>
)
}
```
---
## Pattern 3: Granular Cache Invalidation
Tag caches at multiple levels for precise invalidation:
```tsx
// Cached with multiple tags
async function BlogPost({ postId }: { postId: string }) {
'use cache'
cacheTag('posts', `post-${postId}`)
cacheLife('days')
const post = await db.posts.findUnique({
where: { id: postId },
include: { author: true, comments: true },
})
return <Article post={post} />
}
async function AuthorPosts({ authorId }: { authorId: string }) {
'use cache'
cacheTag('posts', `author-${authorId}`)
cacheLife('hours')
const posts = await db.posts.findMany({
where: { authorId },
})
return <PostGrid posts={posts} />
}
// Server actions with targeted invalidation
'use server'
import { updateTag } from 'next/cache'
export async function updatePost(postId: string, data: FormData) {
const post = await db.posts.update({
where: { id: postId },
data: { title: data.get('title'), content: data.get('content') },
})
// Invalidate specific post only
updateTag(`post-${postId}`)
}
export async function deleteAuthorPosts(authorId: string) {
await db.posts.deleteMany({ where: { authorId } })
// Invalidate all author's posts
updateTag(`author-${authorId}`)
}
export async function clearAllPosts() {
await db.posts.deleteMany()
// Nuclear option - invalidate everything tagged 'posts'
updateTag('posts')
}
```
---
## Pattern 4: Cached Data Fetching Functions
Create reusable cached data fetchers:
```tsx
// lib/data.ts
import { cacheTag, cacheLife } from 'next/cache'
export async function getUser(userId: string) {
'use cache'
cacheTag('users', `user-${userId}`)
cacheLife('hours')
return db.users.findUnique({ where: { id: userId } })
}
export async function getPostsByCategory(category: string) {
'use cache'
cacheTag('posts', `category-${category}`)
cacheLife('minutes')
return db.posts.findMany({
where: { category },
orderBy: { createdAt: 'desc' },
})
}
export async function getPopularProducts() {
'use cache'
cacheTag('products', 'popular')
cacheLife('hours')
return db.products.findMany({
orderBy: { salesCount: 'desc' },
take: 10,
})
}
// Usage in components
async function Sidebar() {
const popular = await getPopularProducts()
return <ProductList products={popular} />
}
```
---
## Pattern 5: Stale-While-Revalidate for Background Updates
Use `revalidateTag` for non-critical updates:
```tsx
// For background analytics or non-user-facing updates
'use server'
import { revalidateTag } from 'next/cache'
export async function trackView(postId: string) {
await db.posts.update({
where: { id: postId },
data: { views: { increment: 1 } },
})
// Background revalidation - old count shown while updating
revalidateTag(`post-${postId}`, 'max')
}
// For user-facing mutations, use updateTag instead
export async function likePost(postId: string) {
await db.likes.create({ data: { postId, userId: getCurrentUserId() } })
// Immediate - user sees their like right away
updateTag(`post-${postId}`)
}
```
---
## Pattern 6: Conditional Caching Based on Content
Cache based on content characteristics:
```tsx
async function ContentBlock({ id }: { id: string }) {
'use cache'
const content = await db.content.findUnique({ where: { id } })
// Adjust cache life based on content type
if (content.type === 'static') {
cacheLife('max')
cacheTag('static-content')
} else if (content.type === 'news') {
cacheLife('minutes')
cacheTag('news', `news-${id}`)
} else {
cacheLife('default')
cacheTag('content', `content-${id}`)
}
return <ContentRenderer content={content} />
}
```
---
## Pattern 7: Nested Cached Components
Compose cached components for fine-grained caching:
```tsx
// Each component caches independently
async function Header() {
'use cache'
cacheTag('layout', 'header')
cacheLife('days')
const nav = await db.navigation.findFirst()
return <Nav items={nav.items} />
}
async function Footer() {
'use cache'
cacheTag('layout', 'footer')
cacheLife('days')
const footer = await db.footer.findFirst()
return <FooterContent data={footer} />
}
async function Sidebar({ category }: { category: string }) {
'use cache'
cacheTag('sidebar', `category-${category}`)
cacheLife('hours')
const related = await db.posts.findMany({
where: { category },
take: 5,
})
return <RelatedPosts posts={related} />
}
// Page composes cached components
export default async function BlogLayout({
children,
params,
}: {
children: React.ReactNode
params: { category: string }
}) {
return (
<>
<Header />
<main>
{children}
<Sidebar category={params.category} />
</main>
<Footer />
</>
)
}
```
---
## Pattern 8: E-commerce Product Page
Complete example for e-commerce:
```tsx
// app/products/[id]/page.tsx
import { Suspense } from 'react'
import { cacheTag, cacheLife } from 'next/cache'
// Cached product details (changes rarely)
async function ProductDetails({ productId }: { productId: string }) {
'use cache'
cacheTag('products', `product-${productId}`)
cacheLife('hours')
const product = await db.products.findUnique({
where: { id: productId },
include: { images: true, specifications: true },
})
return (
<div>
<ProductGallery images={product.images} />
<ProductInfo product={product} />
<Specifications specs={product.specifications} />
</div>
)
}
// Cached reviews (moderate change frequency)
async function ProductReviews({ productId }: { productId: string }) {
'use cache'
cacheTag(`product-${productId}-reviews`)
cacheLife('minutes')
const reviews = await db.reviews.findMany({
where: { productId },
orderBy: { createdAt: 'desc' },
take: 10,
})
return <ReviewsList reviews={reviews} />
}
// Dynamic inventory (real-time)
async function InventoryStatus({ productId }: { productId: string }) {
// No cache - always fresh
const inventory = await db.inventory.findUnique({
where: { productId },
})
return (
<div>
{inventory.quantity > 0 ? (
<span className="text-green-600">In Stock ({inventory.quantity})</span>
) : (
<span className="text-red-600">Out of Stock</span>
)}
</div>
)
}
// Page composition
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
return (
<>
<ProductDetails productId={id} />
<Suspense fallback={<InventorySkeleton />}>
<InventoryStatus productId={id} />
</Suspense>
{/* Suspense around cached components:
- At BUILD TIME (PPR): Cached content is pre-rendered into the static shell,
so the fallback is never shown for initial page loads.
- At RUNTIME (cache miss/expiration): When the cache expires or on cold start,
Suspense shows the fallback while fresh data loads.
- For long-lived caches ('minutes', 'hours', 'days'), Suspense is optional
but improves UX during the rare cache miss. */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={id} />
</Suspense>
</>
)
}
```
---
## Pattern 9: Multi-tenant SaaS Application
Handle tenant-specific caching:
```tsx
// lib/tenant.ts
export async function getTenantId() {
const host = (await headers()).get('host')
return host?.split('.')[0] // subdomain as tenant ID
}
// Tenant-scoped cached data
async function TenantDashboard({ tenantId }: { tenantId: string }) {
'use cache'
cacheTag(`tenant-${tenantId}`, 'dashboards')
cacheLife('minutes')
const data = await db.dashboards.findFirst({
where: { tenantId },
})
return <Dashboard data={data} />
}
// Page with tenant context
export default function DashboardPage() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<DashboardLoader />
</Suspense>
)
}
async function DashboardLoader() {
const tenantId = await getTenantId()
return <TenantDashboard tenantId={tenantId} />
}
// Tenant-specific invalidation
'use server'
import { updateTag } from 'next/cache'
export async function updateTenantSettings(data: FormData) {
const tenantId = await getTenantId()
await db.settings.update({
where: { tenantId },
data: {
/* ... */
},
})
// Only invalidate this tenant's cache
updateTag(`tenant-${tenantId}`)
}
```
---
## Pattern 10: Subshell Composition with generateStaticParams
Leverage parameter permutations to create reusable subshells:
```tsx
// app/products/[category]/[slug]/page.tsx
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
// Product details - uses both params
async function ProductDetails({
category,
slug,
}: {
category: string
slug: string
}) {
'use cache'
cacheTag('products', `product-${slug}`)
cacheLife('hours')
const product = await db.products.findUnique({
where: { category, slug },
})
return <ProductCard product={product} />
}
export default async function ProductPage({
params,
}: {
params: Promise<{ category: string; slug: string }>
}) {
const { category, slug } = await params
return <ProductDetails category={category} slug={slug} />
}
// Provide params to enable subshell generation
export async function generateStaticParams() {
const products = await db.products.findMany({
select: { category: true, slug: true },
take: 100,
})
return products.map(({ category, slug }) => ({ category, slug }))
}
```
```tsx
// app/products/[category]/layout.tsx
import { Suspense } from 'react'
import { cacheLife, cacheTag } from 'next/cache'
// Category header - uses only category param
async function CategoryHeader({ category }: { category: string }) {
'use cache'
cacheTag('categories', `category-${category}`)
cacheLife('days')
const cat = await db.categories.findUnique({ where: { slug: category } })
return (
<header>
<h1>{cat.name}</h1>
<p>{cat.description}</p>
</header>
)
}
export default async function CategoryLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ category: string }>
}) {
const { category } = await params
return (
<>
<CategoryHeader category={category} />
{/* Suspense enables subshell generation */}
<Suspense fallback={<ProductSkeleton />}>{children}</Suspense>
</>
)
}
```
**Result**: When users navigate to `/products/jackets/unknown-jacket`:
1. Category subshell (`/products/jackets/[slug]`) served instantly
2. Product details stream in as they load
3. Future visits to any jacket product reuse the category shell
---
## Pattern 11: Hierarchical Params for Deep Routes
For deeply nested routes, structure layouts to maximize subshell reuse:
```tsx
// Route: /store/[region]/[category]/[productId]
// app/store/[region]/layout.tsx
export default async function RegionLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ region: string }>
}) {
const { region } = await params
return (
<>
<RegionHeader region={region} /> {/* Cached */}
<RegionPromos region={region} /> {/* Cached */}
<Suspense>{children}</Suspense> {/* Subshell boundary */}
</>
)
}
// app/store/[region]/[category]/layout.tsx
export default async function CategoryLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ region: string; category: string }>
}) {
const { region, category } = await params
return (
<>
<CategoryNav region={region} category={category} /> {/* Cached */}
<Suspense>{children}</Suspense> {/* Subshell boundary */}
</>
)
}
// app/store/[region]/[category]/[productId]/page.tsx
export default async function ProductPage({
params,
}: {
params: Promise<{ region: string; category: string; productId: string }>
}) {
const { region, category, productId } = await params
return <ProductDetails region={region} productId={productId} />
}
export async function generateStaticParams() {
// Return popular products - subshells generated for all unique region/category combos
return [
{ region: 'us', category: 'electronics', productId: 'iphone-16' },
{ region: 'us', category: 'electronics', productId: 'macbook-pro' },
{ region: 'us', category: 'clothing', productId: 'hoodie-xl' },
{ region: 'eu', category: 'electronics', productId: 'iphone-16' },
]
}
```
**Generated subshells:**
- `/store/us/[category]/[productId]` - US region shell
- `/store/eu/[category]/[productId]` - EU region shell
- `/store/us/electronics/[productId]` - US Electronics shell
- `/store/us/clothing/[productId]` - US Clothing shell
- `/store/eu/electronics/[productId]` - EU Electronics shell
---
## When to Use Suspense with Cached Components
Understanding when Suspense is required vs. optional for cached components:
### Dynamic Components (no cache) → Suspense Required
```tsx
// Dynamic content MUST have Suspense for streaming
async function PersonalizedFeed() {
const session = await getSession() // Dynamic - reads cookies
const feed = await fetchFeed(session.userId)
return <Feed posts={feed} />
}
export default function Page() {
return (
<Suspense fallback={<FeedSkeleton />}>
<PersonalizedFeed />
</Suspense>
)
}
```
### Cached Components → Suspense Optional (but recommended)
```tsx
// Cached content: Suspense is optional but improves UX
async function ProductReviews({ productId }: { productId: string }) {
'use cache'
cacheLife('minutes')
const reviews = await fetchReviews(productId)
return <ReviewsList reviews={reviews} />
}
// ✅ With Suspense - handles cache miss gracefully
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={id} />
</Suspense>
// ✅ Without Suspense - also valid for long-lived caches
<ProductReviews productId={id} />
```
### Why Cached Components Don't Always Need Suspense
| Scenario | What Happens | Suspense Needed? |
|----------|--------------|------------------|
| **Build time (PPR enabled)** | Content pre-rendered into static shell | No - fallback never shown |
| **Runtime - cache hit** | Cached result returned immediately | No - no suspension |
| **Runtime - cache miss** | Async function executes, component suspends | Yes - for better UX |
### Recommendations by Cache Lifetime
| Cache Lifetime | Suspense Recommendation | Reasoning |
|----------------|------------------------|-----------|
| `'seconds'` | **Recommended** | Frequent cache misses |
| `'minutes'` | Optional | ~5 min expiry, occasional misses |
| `'hours'` / `'days'` | Optional | Rare cache misses |
| `'max'` | Not needed | Essentially static |
### The Trade-off
**Without Suspense**: On cache miss, the page waits for data before rendering anything downstream. For long-lived caches, this is rare and brief.
**With Suspense**: On cache miss, users see the skeleton immediately while data loads. Better perceived performance, slightly more code.
**Rule of thumb**: When in doubt, add Suspense. It never hurts and handles edge cases gracefully.
---
## Anti-Patterns to Avoid
### ❌ Caching user-specific data without parameters
```tsx
// BAD: Same cache for all users
async function UserProfile() {
'use cache'
const user = await getCurrentUser() // Different per user!
return <Profile user={user} />
}
// GOOD: User ID as parameter (becomes cache key)
async function UserProfile({ userId }: { userId: string }) {
'use cache'
cacheTag(`user-${userId}`)
const user = await db.users.findUnique({ where: { id: userId } })
return <Profile user={user} />
}
```
### ❌ Over-caching volatile data
```tsx
// BAD: Caching real-time data
async function StockPrice({ symbol }: { symbol: string }) {
'use cache'
cacheLife('hours') // Stale prices!
return await fetchStockPrice(symbol)
}
// GOOD: Don't cache, or use very short cache
async function StockPrice({ symbol }: { symbol: string }) {
'use cache'
cacheLife('seconds') // 1 second max
return await fetchStockPrice(symbol)
}
// BETTER: No cache for truly real-time
async function StockPrice({ symbol }: { symbol: string }) {
return await fetchStockPrice(symbol)
}
```
### ❌ Forgetting Suspense for dynamic content
```tsx
// BAD: No fallback for DYNAMIC content - breaks streaming
export default async function Page() {
return (
<>
<CachedHeader />
<DynamicContent /> {/* Dynamic - NEEDS Suspense */}
</>
)
}
// GOOD: Proper Suspense boundary for dynamic content
export default async function Page() {
return (
<>
<CachedHeader />
<Suspense fallback={<ContentSkeleton />}>
<DynamicContent />
</Suspense>
</>
)
}
// ALSO GOOD: Cached content without Suspense (optional for long-lived caches)
export default async function Page() {
return (
<>
<CachedHeader /> {/* 'use cache' - no Suspense needed */}
<CachedSidebar /> {/* 'use cache' - no Suspense needed */}
<Suspense fallback={<ContentSkeleton />}>
<DynamicContent /> {/* Dynamic - Suspense required */}
</Suspense>
</>
)
}
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,467 @@
---
name: cache-components
description: |
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR).
**PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations.
**DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions.
**USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.
---
# Next.js Cache Components
> **Auto-activation**: This skill activates automatically in projects with `cacheComponents: true` in next.config.
## Project Detection
When starting work in a Next.js project, check if Cache Components are enabled:
```bash
# Check next.config.ts or next.config.js for cacheComponents
grep -r "cacheComponents" next.config.* 2>/dev/null
```
If `cacheComponents: true` is found, apply this skill's patterns proactively when:
- Writing React Server Components
- Implementing data fetching
- Creating Server Actions with mutations
- Optimizing page performance
- Reviewing existing component code
Cache Components enable **Partial Prerendering (PPR)** - mixing static HTML shells with dynamic streaming content for optimal performance.
## Philosophy: Code Over Configuration
Cache Components represents a shift from **segment configuration** to **compositional code**:
| Before (Deprecated) | After (Cache Components) |
| --------------------------------------- | ----------------------------------------- |
| `export const revalidate = 3600` | `cacheLife('hours')` inside `'use cache'` |
| `export const dynamic = 'force-static'` | Use `'use cache'` and Suspense boundaries |
| All-or-nothing static/dynamic | Granular: static shell + cached + dynamic |
**Key Principle**: Components co-locate their caching, not just their data. Next.js provides build-time feedback to guide you toward optimal patterns.
## Core Concept
```
┌─────────────────────────────────────────────────────┐
│ Static Shell │
│ (Sent immediately to browser) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Header │ │ Cached │ │ Suspense │ │
│ │ (static) │ │ Content │ │ Fallback │ │
│ └─────────────┘ └─────────────┘ └──────┬──────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ Dynamic │ │
│ │ (streams) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘
```
## Mental Model: The Caching Decision Tree
When writing a React Server Component, ask these questions in order:
```
┌─────────────────────────────────────────────────────────┐
│ Does this component fetch data or perform I/O? │
└─────────────────────┬───────────────────────────────────┘
┌──────────▼──────────┐
│ YES │ NO → Pure component, no action needed
└──────────┬──────────┘
┌─────────────────▼─────────────────┐
│ Does it depend on request context? │
│ (cookies, headers, searchParams) │
└─────────────────┬─────────────────┘
┌────────────┴────────────┐
│ │
┌────▼────┐ ┌─────▼─────┐
│ YES │ │ NO │
└────┬────┘ └─────┬─────┘
│ │
│ ┌─────▼─────────────────┐
│ │ Can this be cached? │
│ │ (same for all users?) │
│ └─────┬─────────────────┘
│ │
│ ┌──────────┴──────────┐
│ │ │
│ ┌────▼────┐ ┌─────▼─────┐
│ │ YES │ │ NO │
│ └────┬────┘ └─────┬─────┘
│ │ │
│ ▼ │
│ 'use cache' │
│ + cacheTag() │
│ + cacheLife() │
│ │
└──────────────┬─────────────────────┘
Wrap in <Suspense>
(dynamic streaming)
```
**Key insight**: The `'use cache'` directive is for data that's the _same across users_. User-specific data stays dynamic with Suspense.
## Quick Start
### Enable Cache Components
```typescript
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
```
### Basic Usage
```tsx
// Cached component - output included in static shell
async function CachedPosts() {
'use cache'
const posts = await db.posts.findMany()
return <PostList posts={posts} />
}
// Page with static + cached + dynamic content
export default async function BlogPage() {
return (
<>
<Header /> {/* Static */}
<CachedPosts /> {/* Cached */}
<Suspense fallback={<Skeleton />}>
<DynamicComments /> {/* Dynamic - streams */}
</Suspense>
</>
)
}
```
## Core APIs
### 1. `'use cache'` Directive
Marks code as cacheable. Can be applied at three levels:
```tsx
// File-level: All exports are cached
'use cache'
export async function getData() {
/* ... */
}
export async function Component() {
/* ... */
}
// Component-level
async function UserCard({ id }: { id: string }) {
'use cache'
const user = await fetchUser(id)
return <Card>{user.name}</Card>
}
// Function-level
async function fetchWithCache(url: string) {
'use cache'
return fetch(url).then((r) => r.json())
}
```
**Important**: All cached functions must be `async`.
### 2. `cacheLife()` - Control Cache Duration
```tsx
import { cacheLife } from 'next/cache'
async function Posts() {
'use cache'
cacheLife('hours') // Use a predefined profile
// Or custom configuration:
cacheLife({
stale: 60, // 1 min - client cache validity
revalidate: 3600, // 1 hr - start background refresh
expire: 86400, // 1 day - absolute expiration
})
return await db.posts.findMany()
}
```
**Predefined profiles**: `'default'`, `'seconds'`, `'minutes'`, `'hours'`, `'days'`, `'weeks'`, `'max'`
### 3. `cacheTag()` - Tag for Invalidation
```tsx
import { cacheTag } from 'next/cache'
async function BlogPosts() {
'use cache'
cacheTag('posts')
cacheLife('days')
return await db.posts.findMany()
}
async function UserProfile({ userId }: { userId: string }) {
'use cache'
cacheTag('users', `user-${userId}`) // Multiple tags
return await db.users.findUnique({ where: { id: userId } })
}
```
### 4. `updateTag()` - Immediate Invalidation
For **read-your-own-writes** semantics:
```tsx
'use server'
import { updateTag } from 'next/cache'
export async function createPost(formData: FormData) {
await db.posts.create({ data: formData })
updateTag('posts') // Client immediately sees fresh data
}
```
### 5. `revalidateTag()` - Background Revalidation
For stale-while-revalidate pattern:
```tsx
'use server'
import { revalidateTag } from 'next/cache'
export async function updatePost(id: string, data: FormData) {
await db.posts.update({ where: { id }, data })
revalidateTag('posts', 'max') // Serve stale, refresh in background
}
```
## When to Use Each Pattern
| Content Type | API | Behavior |
| ------------ | ------------------- | ------------------------------------- |
| **Static** | No directive | Rendered at build time |
| **Cached** | `'use cache'` | Included in static shell, revalidates |
| **Dynamic** | Inside `<Suspense>` | Streams at request time |
## Parameter Permutations & Subshells
**Critical Concept**: With Cache Components, Next.js renders ALL permutations of provided parameters to create reusable subshells.
```tsx
// app/products/[category]/[slug]/page.tsx
export async function generateStaticParams() {
return [
{ category: 'jackets', slug: 'classic-bomber' },
{ category: 'jackets', slug: 'essential-windbreaker' },
{ category: 'accessories', slug: 'thermal-fleece-gloves' },
]
}
```
Next.js renders these routes:
```
/products/jackets/classic-bomber ← Full params (complete page)
/products/jackets/essential-windbreaker ← Full params (complete page)
/products/accessories/thermal-fleece-gloves ← Full params (complete page)
/products/jackets/[slug] ← Partial params (category subshell)
/products/accessories/[slug] ← Partial params (category subshell)
/products/[category]/[slug] ← No params (fallback shell)
```
**Why this matters**: The category subshell (`/products/jackets/[slug]`) can be reused for ANY jacket product, even ones not in `generateStaticParams`. Users navigating to an unlisted jacket get the cached category shell immediately, with product details streaming in.
### `generateStaticParams` Requirements
With Cache Components enabled:
1. **Must provide at least one parameter** - Empty arrays now cause build errors (prevents silent production failures)
2. **Params prove static safety** - Providing params lets Next.js verify no dynamic APIs are called
3. **Partial params create subshells** - Each unique permutation generates a reusable shell
```tsx
// ❌ ERROR with Cache Components
export function generateStaticParams() {
return [] // Build error: must provide at least one param
}
// ✅ CORRECT: Provide real params
export async function generateStaticParams() {
const products = await getPopularProducts()
return products.map(({ category, slug }) => ({ category, slug }))
}
```
## Cache Key = Arguments
Arguments become part of the cache key:
```tsx
// Different userId = different cache entry
async function UserData({ userId }: { userId: string }) {
'use cache'
cacheTag(`user-${userId}`)
return await fetchUser(userId)
}
```
## Build-Time Feedback
Cache Components provides early feedback during development. These build errors **guide you toward optimal patterns**:
### Error: Dynamic data outside Suspense
```
Error: Accessing cookies/headers/searchParams outside a Suspense boundary
```
**Solution**: Wrap dynamic components in `<Suspense>`:
```tsx
<Suspense fallback={<Skeleton />}>
<ComponentThatUsesCookies />
</Suspense>
```
### Error: Uncached data outside Suspense
```
Error: Accessing uncached data outside Suspense
```
**Solution**: Either cache the data or wrap in Suspense:
```tsx
// Option 1: Cache it
async function ProductData({ id }: { id: string }) {
'use cache'
return await db.products.findUnique({ where: { id } })
}
// Option 2: Make it dynamic with Suspense
;<Suspense fallback={<Loading />}>
<DynamicProductData id={id} />
</Suspense>
```
### Error: Request data inside cache
```
Error: Cannot access cookies/headers inside 'use cache'
```
**Solution**: Extract runtime data outside cache boundary (see "Handling Runtime Data" above).
## Additional Resources
- For complete API reference, see [REFERENCE.md](REFERENCE.md)
- For common patterns and recipes, see [PATTERNS.md](PATTERNS.md)
- For debugging and troubleshooting, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
## Code Generation Guidelines
When generating Cache Component code:
1. **Always use `async`** - All cached functions must be async
2. **Place `'use cache'` first** - Must be first statement in function body
3. **Call `cacheLife()` early** - Should follow `'use cache'` directive
4. **Tag meaningfully** - Use semantic tags that match your invalidation needs
5. **Extract runtime data** - Move `cookies()`/`headers()` outside cached scope
6. **Wrap dynamic content** - Use `<Suspense>` for non-cached async components
---
## Proactive Application (When Cache Components Enabled)
When `cacheComponents: true` is detected in the project, **automatically apply these patterns**:
### When Writing Data Fetching Components
Ask yourself: "Can this data be cached?" If yes, add `'use cache'`:
```tsx
// Before: Uncached fetch
async function ProductList() {
const products = await db.products.findMany()
return <Grid products={products} />
}
// After: With caching
async function ProductList() {
'use cache'
cacheTag('products')
cacheLife('hours')
const products = await db.products.findMany()
return <Grid products={products} />
}
```
### When Writing Server Actions
Always invalidate relevant caches after mutations:
```tsx
'use server'
import { updateTag } from 'next/cache'
export async function createProduct(data: FormData) {
await db.products.create({ data })
updateTag('products') // Don't forget!
}
```
### When Composing Pages
Structure with static shell + cached content + dynamic streaming:
```tsx
export default async function Page() {
return (
<>
<StaticHeader /> {/* No cache needed */}
<CachedContent /> {/* 'use cache' */}
<Suspense fallback={<Skeleton />}>
<DynamicUserContent /> {/* Streams at runtime */}
</Suspense>
</>
)
}
```
### When Reviewing Code
Flag these issues in Cache Components projects:
- [ ] Data fetching without `'use cache'` where caching would benefit
- [ ] Missing `cacheTag()` calls (makes invalidation impossible)
- [ ] Missing `cacheLife()` (relies on defaults which may not be appropriate)
- [ ] Server Actions without `updateTag()`/`revalidateTag()` after mutations
- [ ] `cookies()`/`headers()` called inside `'use cache'` scope
- [ ] Dynamic components without `<Suspense>` boundaries
- [ ] **DEPRECATED**: `export const revalidate` - replace with `cacheLife()` in `'use cache'`
- [ ] **DEPRECATED**: `export const dynamic` - replace with Suspense + cache boundaries
- [ ] Empty `generateStaticParams()` return - must provide at least one param

View File

@@ -0,0 +1,719 @@
# Cache Components Troubleshooting
Common issues, debugging techniques, and solutions for Cache Components.
## Build-Time Feedback Philosophy
Cache Components introduces **early feedback** during development. Unlike before where errors might only appear in production, Cache Components produces build errors that **guide you toward optimal patterns**.
Key principle: **If it builds, it's correct.** The build process validates that:
- Dynamic data isn't accessed outside Suspense boundaries
- Cached data doesn't depend on request-specific APIs
- `generateStaticParams` provides valid parameters to test rendering
---
## Quick Debugging Checklist
Copy this checklist when debugging cache issues:
### Cache Not Working
- [ ] `cacheComponents: true` in next.config?
- [ ] Function is `async`?
- [ ] `'use cache'` is FIRST statement in function body?
- [ ] All arguments are serializable (no functions, class instances)?
- [ ] Not accessing `cookies()`/`headers()` inside cache?
### Stale Data After Mutation
- [ ] Called `updateTag()` or `revalidateTag()` after mutation?
- [ ] Tag in invalidation matches tag in `cacheTag()`?
- [ ] Using `updateTag()` (not `revalidateTag()`) for immediate updates?
### Build Errors
- [ ] Dynamic data wrapped in `<Suspense>`?
- [ ] `generateStaticParams` returns at least one param?
- [ ] Not mixing `'use cache'` with `cookies()`/`headers()`?
### Performance Issues
- [ ] Cache granularity appropriate? (not too coarse/fine)
- [ ] `cacheLife` set appropriately for data volatility?
- [ ] Using hierarchical tags for targeted invalidation?
---
## Error: UseCacheTimeoutError
### Symptoms
```
Error: A component used 'use cache' but didn't complete within 50 seconds.
```
### Cause
The cached function is accessing request-specific data (cookies, headers, searchParams) or making requests that depend on runtime context.
### Solution
User-specific content that depends on runtime data (cookies, headers, searchParams) should **not be cached**. Instead, stream it dynamically:
```tsx
// ❌ WRONG: Trying to cache user-specific content
async function UserContent() {
'use cache'
const session = await cookies() // Causes timeout!
return await fetchContent(session.userId)
}
// ✅ CORRECT: Don't cache user-specific content, stream it instead
async function UserContent() {
const session = await cookies()
return await fetchContent(session.get('userId')?.value)
}
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<UserContent /> {/* No 'use cache' - streams dynamically */}
</Suspense>
)
}
```
**Key insight**: Cache Components are for content that can be shared across users (e.g., product details, blog posts). User-specific content should stream at request time.
---
## Error: Cannot use 'use cache' with sync function
### Symptoms
```
Error: 'use cache' can only be used in async functions
```
### Cause
Cache Components require async functions because cached outputs are streamed.
### Solution
```tsx
// ❌ WRONG: Synchronous function
function CachedComponent() {
'use cache'
return <div>Hello</div>
}
// ✅ CORRECT: Async function
async function CachedComponent() {
'use cache'
return <div>Hello</div>
}
```
---
## Error: Dynamic Data Outside Suspense
### Symptoms
```
Error: Accessing cookies/headers/searchParams outside a Suspense boundary
```
### Cause
With Cache Components, accessing request-specific APIs (cookies, headers, searchParams, connection) requires a Suspense boundary so Next.js can provide a static fallback.
### Why This Changed
**Before Cache Components**: The page silently became fully dynamic - no static content served.
**After Cache Components**: Build error ensures you explicitly handle the dynamic boundary.
### Solution
Wrap dynamic content in Suspense:
```tsx
// ❌ ERROR: No Suspense boundary
export default async function Page() {
return (
<>
<Header />
<UserDeals /> {/* Uses cookies() */}
</>
)
}
// ✅ CORRECT: Suspense provides static fallback
export default async function Page() {
return (
<>
<Header />
<Suspense fallback={<DealsSkeleton />}>
<UserDeals />
</Suspense>
</>
)
}
```
> **See also**: Pattern 1 (Static + Cached + Dynamic Page) in PATTERNS.md shows the foundational Suspense boundary pattern.
---
## Error: Uncached Data Outside Suspense
### Symptoms
```
Error: Accessing uncached data outside Suspense
```
### Cause
With Cache Components, ALL **async** I/O is considered dynamic by default. Database queries, fetch calls, and file reads must either be cached or wrapped in Suspense.
> **Note on synchronous databases**: Libraries with synchronous APIs (e.g., `better-sqlite3`) don't trigger this error because they don't involve async I/O. Synchronous operations complete during render and are included in the static shell. However, this also means they block the render thread - use judiciously for small, fast queries only.
### Solution
Either cache the data or wrap in Suspense:
```tsx
// ❌ ERROR: Uncached database query without Suspense
export default async function ProductPage({ params }) {
const product = await db.products.findUnique({ where: { id: params.id } })
return <ProductCard product={product} />
}
// ✅ OPTION 1: Cache the data
async function getProduct(id: string) {
'use cache'
cacheTag(`product-${id}`)
cacheLife('hours')
return await db.products.findUnique({ where: { id } })
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id)
return <ProductCard product={product} />
}
// ✅ OPTION 2: Wrap in Suspense (streams dynamically)
export default async function ProductPage({ params }) {
return (
<Suspense fallback={<ProductSkeleton />}>
<ProductContent id={params.id} />
</Suspense>
)
}
```
> **See also**: Pattern 5 (Cached Data Fetching Functions) in PATTERNS.md shows reusable cached data fetcher patterns.
---
## Error: Empty generateStaticParams
### Symptoms
```
Error: generateStaticParams must return at least one parameter set
```
### Cause
With Cache Components, empty `generateStaticParams` is no longer allowed. This prevents a class of bugs where dynamic API usage in components would only error in production.
### Why This Changed
**Before**: Empty array = "trust me, this is static". Dynamic API usage in production caused runtime errors.
**After**: Must provide at least one param set so Next.js can validate the page actually renders statically.
### Solution
```tsx
// ❌ ERROR: Empty array
export function generateStaticParams() {
return []
}
// ✅ CORRECT: Provide at least one param
export async function generateStaticParams() {
const products = await getPopularProducts()
return products.map(({ category, slug }) => ({ category, slug }))
}
// ✅ ALSO CORRECT: Hardcoded for known routes
export function generateStaticParams() {
return [{ slug: 'about' }, { slug: 'contact' }, { slug: 'pricing' }]
}
```
---
## Error: Request Data Inside Cache
### Symptoms
```
Error: Cannot access cookies/headers inside 'use cache'
```
### Cause
Cache contexts cannot depend on request-specific data because the cached result would be shared across all users.
### Solution
User-specific content should **not be cached**. Remove `'use cache'` and stream the content dynamically:
```tsx
// ❌ ERROR: Cookies inside cache
async function UserDashboard() {
'use cache'
const session = await cookies() // Error!
return await fetchDashboard(session.get('userId'))
}
// ✅ CORRECT: Don't cache user-specific content
async function UserDashboard() {
const session = await cookies()
return await fetchDashboard(session.get('userId')?.value)
}
export default function Page() {
return (
<Suspense fallback={<DashboardSkeleton />}>
<UserDashboard /> {/* Streams at request time */}
</Suspense>
)
}
```
**Key insight**: Cache Components are for content that can be shared across users. User-specific dashboards should stream dynamically.
---
## Issue: Cache Not Being Used
### Symptoms
- Data always fresh on every request
- No caching behavior observed
- Build logs don't show cached routes
### Checklist
**1. Is `cacheComponents` enabled?**
```typescript
// next.config.ts
const nextConfig: NextConfig = {
cacheComponents: true, // Required!
}
```
**2. Is the function async?**
```tsx
// Must be async
async function CachedData() {
'use cache'
return await fetchData()
}
```
**3. Is `'use cache'` the first statement?**
```tsx
// ❌ WRONG: Directive not first
async function CachedData() {
const x = 1 // Something before 'use cache'
;('use cache')
return await fetchData()
}
// ✅ CORRECT: Directive first
async function CachedData() {
'use cache'
const x = 1
return await fetchData()
}
```
**4. Are arguments serializable?**
```tsx
// ❌ WRONG: Function as argument (not serializable)
async function CachedData({ transform }: { transform: (x: any) => any }) {
'use cache'
const data = await fetchData()
return transform(data)
}
// ✅ CORRECT: Only serializable arguments
async function CachedData({ transformType }: { transformType: string }) {
'use cache'
const data = await fetchData()
return applyTransform(data, transformType)
}
```
---
## Issue: Stale Data After Mutation
### Symptoms
- Created/updated data doesn't appear immediately
- Need to refresh page to see changes
### Cause
Cache not invalidated after mutation.
### Solutions
**1. Use `updateTag()` for immediate consistency:**
```tsx
'use server'
import { updateTag } from 'next/cache'
export async function createPost(data: FormData) {
await db.posts.create({ data })
updateTag('posts') // Immediate invalidation
}
```
**2. Ensure tags match:**
```tsx
// Cache uses this tag
async function Posts() {
'use cache'
cacheTag('posts') // Must match invalidation tag
return await db.posts.findMany()
}
// Invalidation must use same tag
export async function createPost(data: FormData) {
await db.posts.create({ data })
updateTag('posts') // Same tag!
}
```
**3. Invalidate all relevant tags:**
```tsx
export async function updatePost(postId: string, data: FormData) {
const post = await db.posts.update({
where: { id: postId },
data,
})
// Invalidate all affected caches
updateTag('posts') // All posts list
updateTag(`post-${postId}`) // Specific post
updateTag(`author-${post.authorId}`) // Author's posts
}
```
---
## Issue: Different Cache Values for Same Key
### Symptoms
- Cache returns different values for what should be the same query
- Inconsistent behavior across requests
### Cause
Arguments are part of cache key. Different argument values = different cache entries.
### Solution
Normalize arguments:
```tsx
// ❌ Problem: Object reference differs
async function CachedData({ options }: { options: { limit: number } }) {
'use cache'
return await fetchData(options)
}
// Each call creates new object = new cache key
<CachedData options={{ limit: 10 }} />
<CachedData options={{ limit: 10 }} /> // Different cache entry!
// ✅ Solution: Use primitives or stable references
async function CachedData({ limit }: { limit: number }) {
'use cache'
return await fetchData({ limit })
}
<CachedData limit={10} />
<CachedData limit={10} /> // Same cache entry!
```
---
## Issue: Cache Too Aggressive (Stale Data)
### Symptoms
- Data doesn't update when expected
- Users see outdated content
### Solutions
**1. Reduce cache lifetime:**
```tsx
async function FrequentlyUpdatedData() {
'use cache'
cacheLife('seconds') // Short cache
// Or custom short duration
cacheLife({
stale: 0,
revalidate: 30,
expire: 60,
})
return await fetchData()
}
```
**2. Don't cache volatile data:**
```tsx
// For truly real-time data, skip caching
async function LiveData() {
// No 'use cache'
return await fetchLiveData()
}
export default function Page() {
return (
<Suspense fallback={<Loading />}>
<LiveData />
</Suspense>
)
}
```
---
## Issue: Build Takes Too Long
### Symptoms
- Build hangs during prerendering
- Timeout errors during `next build`
### Cause
Cached functions making slow network requests or accessing unavailable services during build.
### Solutions
**1. Use fallback data for build:**
```tsx
async function CachedData() {
'use cache'
try {
return await fetchFromAPI()
} catch (error) {
// Return fallback during build if API unavailable
return getFallbackData()
}
}
```
**2. Limit static generation scope:**
```tsx
// app/[slug]/page.tsx
export function generateStaticParams() {
// Only prerender most important pages at build time
// Other pages will be generated on-demand at request time
return [{ slug: 'home' }, { slug: 'about' }]
}
```
**3. Use Suspense for truly dynamic content:**
```tsx
// app/[slug]/page.tsx
import { Suspense } from 'react'
export default function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
return (
<Suspense fallback={<PageSkeleton />}>
<DynamicContent params={params} />
</Suspense>
)
}
```
> **Note:** Avoid using `export const dynamic = 'force-dynamic'` as this segment config is deprecated with Cache Components. Use Suspense boundaries and `'use cache'` for granular control instead.
---
## Debugging Techniques
### 1. Check Cache Headers
In development, inspect response headers:
```bash
curl -I http://localhost:3000/your-page
```
Look for:
- `x-nextjs-cache: HIT` - Served from cache
- `x-nextjs-cache: MISS` - Cache miss, recomputed
- `x-nextjs-cache: STALE` - Stale content, revalidating
### 2. Enable Verbose Logging
```bash
# Environment variable for cache debugging
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
```
### 3. Check Build Output
```bash
npm run build
# Look for:
# ○ (Static) - Fully static
# ◐ (Partial) - Partial prerender with cache
# λ (Dynamic) - Server-rendered
```
### 4. Inspect Cache Tags
Add logging to verify tags:
```tsx
async function CachedData({ id }: { id: string }) {
'use cache'
const tags = ['data', `item-${id}`]
console.log('Cache tags:', tags) // Check during build
tags.forEach((tag) => cacheTag(tag))
cacheLife('hours')
return await fetchData(id)
}
```
---
## Common Mistakes Checklist
| Mistake | Symptom | Fix |
| ---------------------------------- | ------------------ | --------------------- |
| Missing `cacheComponents: true` | No caching | Add to next.config.ts |
| Sync function with `'use cache'` | Build error | Make function async |
| `'use cache'` not first statement | Cache ignored | Move to first line |
| Accessing cookies/headers in cache | Timeout error | Extract to wrapper |
| Non-serializable arguments | Inconsistent cache | Use primitives |
| Missing Suspense for dynamic | Streaming broken | Wrap in Suspense |
| Wrong tag in invalidation | Stale data | Match cache tags |
| Over-caching volatile data | Stale data | Reduce cacheLife |
---
## Performance Optimization Tips
### 1. Profile Cache Hit Rates
Monitor cache effectiveness:
```tsx
async function CachedData() {
'use cache'
const start = performance.now()
const data = await fetchData()
const duration = performance.now() - start
// Log for analysis
console.log(`Cache execution: ${duration}ms`)
return data
}
```
### 2. Optimize Cache Granularity
```tsx
// ❌ Coarse: One big cached component
async function PageContent() {
'use cache'
const header = await fetchHeader()
const posts = await fetchPosts()
const sidebar = await fetchSidebar()
return <>{/* everything */}</>
}
// ✅ Fine-grained: Independent cached components
async function Header() {
'use cache'
cacheLife('days')
return await fetchHeader()
}
async function Posts() {
'use cache'
cacheLife('hours')
return await fetchPosts()
}
async function Sidebar() {
'use cache'
cacheLife('minutes')
return await fetchSidebar()
}
```
### 3. Strategic Tag Design
```tsx
// Hierarchical tags for targeted invalidation
cacheTag(
'posts', // All posts
`category-${category}`, // Posts in category
`post-${id}`, // Specific post
`author-${authorId}` // Author's posts
)
// Invalidate at appropriate level
updateTag(`post-${id}`) // Single post changed
updateTag(`author-${author}`) // Author updated all posts
updateTag('posts') // Nuclear option
```