Loading...
Loading...
A comprehensive guide to Next.js performance patterns, combining Data Fetching Colocation, the Donut Pattern, and the 'use cache' directive for optimal application architecture.
npx skill4agent add violabg/dev-recruit nextjs-performance-architectureuse cachePrerequisites: Next.js 16+ withenabled incacheComponents: true.next.config.ts
┌─────────────────────────────────────────────────────────────────┐
│ Component Rendering Decision │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────┐
│ Does it need user state, │
│ event handlers, or hooks? │
└─────────────────────────────┘
│ │
Yes No
│ │
▼ ▼
┌────────────┐ ┌─────────────────────┐
│ "use │ │ Keep as Server │
│ client" │ │ Component │
└────────────┘ └─────────────────────┘
│
▼
┌─────────────────────────────┐
│ Does it fetch data or do │
│ expensive computation? │
└─────────────────────────────┘
│ │
Yes No
│ │
▼ ▼
┌──────────────────────┐ Static in shell
│ Is data user/request │ (automatic)
│ specific? │
└──────────────────────┘
│ │
Yes No
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Wrap in │ │ Add │
│ <Suspense> │ │ "use cache" │
└─────────────┘ └─────────────┘async// ❌ Before: Prop drilling blocks parallelism
export default async function Page() {
const data = await getData();
return <Child data={data} />;
}
// ✅ After: Collocated fetching enables parallel loading
export default async function Child() {
const data = await getData();
return <div>{data.title}</div>;
}use()React.use()// Server Component
export default function Page() {
const userPromise = getUser(); // Don't await!
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Client Component
"use client";
import { use } from "react";
export function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // Suspends until resolved
return <div>{user.name}</div>;
}useEffectuseState"use client"asyncchildren// AnimatedContainer.tsx (Client Component - the "donut")
"use client";
import { motion } from "framer-motion";
export function AnimatedContainer({ children }: { children: React.ReactNode }) {
return (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
{children}
</motion.div>
);
}
// Page.tsx (Server Component)
import { AnimatedContainer } from "./AnimatedContainer";
import { ProductList } from "./ProductList"; // Server Component
export default function Page() {
return (
<AnimatedContainer>
{/* ProductList runs on server, not included in client bundle */}
<ProductList />
</AnimatedContainer>
);
}async"use client"use cachenext.config.tsimport type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;| Scenario | Solution |
|---|---|
| Needs request data (cookies, headers, user-specific) | Wrap in |
| Expensive but static/shared | Add |
| Mix of both | Combine patterns (Donut + Cache) |
// File-level caching (all exports cached)
"use cache";
export async function getProducts() {
return await db.product.findMany();
}
export async function getCategories() {
return await db.category.findMany();
}// Function-level caching
export async function getFeaturedSkills() {
"use cache";
return await db.skill.findMany({ where: { featured: true } });
}cacheLife()| Profile | Use Case | Stale | Revalidate | Expire |
|---|---|---|---|---|
| Real-time data (stock prices) | 0s | 1s | 60s |
| Frequently updated (feeds) | 5min | 1min | 1h |
| Moderately static (blog posts) | 5min | 1h | 1d |
| Rarely changing (product catalog) | 5min | 1d | 1w |
| Very stable (landing pages) | 5min | 1w | 1mo |
| Immutable (versioned assets) | 5min | 1y | indefinite |
import { cacheLife } from "next/cache";
export async function ProductCatalog() {
"use cache";
cacheLife("days"); // Cache for ~1 day
const products = await db.product.findMany();
return <ProductGrid products={products} />;
}import { cacheLife, cacheTag } from "next/cache";
async function getPostContent(slug: string) {
"use cache";
cacheTag(`post-${slug}`);
const post = await fetchPost(slug);
if (!post) {
cacheLife("minutes"); // Missing content, check again soon
return null;
}
cacheLife("days"); // Published content, cache longer
return post.data;
}cacheTag()import { cacheTag } from "next/cache";
export async function getSkillById(id: string) {
"use cache";
cacheTag("skills", `skill-${id}`);
return await db.skill.findUnique({ where: { id } });
}updateTag()"use server";
import { updateTag } from "next/cache";
export async function updateSkill(id: string, data: SkillData) {
await db.skill.update({ where: { id }, data });
updateTag(`skill-${id}`); // Invalidate specific skill immediately
updateTag("skills"); // Invalidate all skills
}vsupdateTag:revalidateTag
— Use in Server Actions for read-your-own-writes (user sees changes immediately)updateTag — Use in Route Handlers, webhooks, or when stale-while-revalidate is acceptablerevalidateTag
| Don't | Do Instead |
|---|---|
| |
| Add |
| Use |
| Reading cookies/headers inside cached scope | Read outside, pass as arguments |
// app/products/[id]/page.tsx (Server Component)
import { Suspense } from "react";
import { ProductDetails } from "@/components/ProductDetails";
import { AddToCartButton } from "@/components/AddToCartButton";
import { RecommendedProducts } from "@/components/RecommendedProducts";
import { ProductSkeleton, RecommendedSkeleton } from "@/components/skeletons";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
return (
<div className="grid grid-cols-3 gap-6">
{/* Cached: Product details rarely change */}
<div className="col-span-2">
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails id={id} />
</Suspense>
</div>
{/* Dynamic: User-specific cart state (Donut Pattern) */}
<aside>
<AddToCartButton productId={id} />
</aside>
{/* Cached with Suspense: Recommendations can stream in */}
<div className="col-span-3">
<Suspense fallback={<RecommendedSkeleton />}>
<RecommendedProducts productId={id} />
</Suspense>
</div>
</div>
);
}// components/ProductDetails.tsx (Cached Server Component)
import { cacheLife, cacheTag } from "next/cache";
export async function ProductDetails({ id }: { id: string }) {
"use cache";
cacheLife("hours");
cacheTag("products", `product-${id}`);
const product = await db.product.findUnique({ where: { id } });
return (
<article>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span className="text-2xl font-bold">${product.price}</span>
</article>
);
}// components/AddToCartButton.tsx (Client Component - Donut wrapper)
"use client";
import { useState, useTransition } from "react";
import { addToCart } from "@/actions/cart";
export function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition();
const [quantity, setQuantity] = useState(1);
return (
<form
action={() => startTransition(() => addToCart(productId, quantity))}
>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
min={1}
/>
<button disabled={isPending}>
{isPending ? "Adding..." : "Add to Cart"}
</button>
</form>
);
}| Scenario | Suspense Needed? |
|---|---|
Cached component ( | Usually not needed (part of static shell) |
| Dynamic data (user-specific) | Yes - shows fallback while loading |
| Streaming async Server Component | Yes - prevents blocking |
Client Component with | Yes - parent must provide boundary |
// ❌ Coarse: Entire page waits for all data
<Suspense fallback={<FullPageSkeleton />}>
<Header />
<MainContent />
<Sidebar />
</Suspense>
// ✅ Granular: Components load independently
<Header /> {/* Static, no Suspense */}
<Suspense fallback={<ContentSkeleton />}>
<MainContent /> {/* Async */}
</Suspense>
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar /> {/* Async, loads parallel to MainContent */}
</Suspense>CACHE HITCACHE MISSCACHE SKIP| Symptom | Likely Cause | Fix |
|---|---|---|
| Component not caching | Accessing request-specific data | Move cookies/headers read outside cached scope |
| Stale data after mutation | Missing | Add proper cache tags and revalidate |
| Hydration mismatch | Date/time in cached component | Use |
Build error with | Edge runtime not supported | Use Node.js runtime only |
next build○●ƒ◐| Pattern | Purpose | Key Directive |
|---|---|---|
| Data Colocation | Fetch where data is used | None (architectural) |
| Donut Pattern | Server content in Client wrapper | |
| Cache Components | Cache expensive computations | |
| Function | Purpose |
|---|---|
| Set cache duration |
| Tag for targeted invalidation |
| Invalidate immediately (Server Actions only) |
| Invalidate with stale-while-revalidate (Route Handlers, webhooks) |