Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
These skill recipes are designed for Claude Code but also work with other AI coding agents that support skill/instruction files.
The complete SKILL.md content you can copy into .claude/skills/nextjs-rendering/SKILL.md:
---
name: nextjs-rendering-strategies
description: "Deep expertise in SSR, SSG, ISR, CSR, and Partial Prerendering decisions for Next.js. Use when asked to: rendering strategy, SSR vs SSG, static vs dynamic, PPR, partial prerendering, generateStaticParams, ISR, CSR."
allowed-tools: "Read, Write, Edit, Glob, Grep, Bash(npm:*), Bash(npx:*), Agent"
---
# Next.js Rendering Strategies
You are a Next.js rendering expert. Help developers choose and implement the optimal rendering strategy for every page.
## Rendering Decision Flowchart
Follow this decision tree for every page:
1. **Does the content change per user or per request?**
- No -> Go to step 2
- Yes -> Go to step 4
2. **Does the content change at all after build?**
- No -> **SSG** (Static Site Generation)
- Yes -> Go to step 3
3. **How often does it change?**
- Predictable interval -> **ISR** (Incremental Static Regeneration)
- Unpredictable (event-driven) -> **ISR with on-demand revalidation**
- Real-time -> **SSR** or **CSR** with polling
4. **Does the page have both static and dynamic parts?**
- Yes -> **PPR** (Partial Prerendering) with Suspense
- No, entirely dynamic -> **SSR** (Server-Side Rendering)
- Client-interactive only -> **CSR** (Client-Side Rendering)
## Strategy Reference
### SSG - Static Site Generation
**When:** Content is the same for all users and does not change after build (or changes very rarely).
**Examples:** Marketing pages, documentation, blog posts, legal pages.
```tsx
// app/about/page.tsx
// This is automatically static because it has no dynamic functions
export default function AboutPage() \{
return <div>About us content...</div>;
\}
// Dynamic routes with generateStaticParams
// app/blog/[slug]/page.tsx
export async function generateStaticParams() \{
const posts = await getAllPosts();
return posts.map((post) => (\{ slug: post.slug \}));
\}
export default async function BlogPost(\{
params,
\}: \{
params: Promise<\{ slug: string \}>;
\}) \{
const \{ slug \} = await params;
const post = await getPost(slug);
return <Article post=\{post\} />;
\}Performance: Best. Served from CDN edge. Zero server compute at request time.
When: Content changes periodically but does not need to be real-time.
Examples: Product catalog, news articles, pricing pages, blog with updates.
// Time-based ISR
// app/products/page.tsx
export const revalidate = 3600; // revalidate every hour
export default async function ProductsPage() \{
const products = await getProducts();
return <ProductGrid products=\{products\} />;
\}
// On-demand ISR via Server Action
// app/actions.ts
"use server";
import \{ revalidatePath, revalidateTag \} from "next/cache";
export async function updateProduct(id: string, data: ProductData) \{
await db.product.update(\{ where: \{ id \}, data \});
revalidateTag("products"); // invalidate all product pages
\}Performance: Near-static. First request after revalidation triggers a rebuild; subsequent requests serve the cached version.
When: Content is different per request (user-specific, real-time data, authentication).
Examples: Dashboards, user profiles, shopping carts, search results.
// app/dashboard/page.tsx
import \{ cookies \} from "next/headers";
export default async function DashboardPage() \{
// Using cookies() makes this route dynamic (SSR)
const cookieStore = await cookies();
const session = cookieStore.get("session");
const user = await getUser(session?.value);
const data = await getDashboardData(user.id);
return <Dashboard user=\{user\} data=\{data\} />;
\}Performance: Slower than static. Every request hits the server. Mitigate with streaming.
When: Highly interactive content, real-time data, or features that do not need SEO.
Examples: Admin panels, data visualization tools, collaborative editors, maps.
"use client";
import useSWR from "swr";
export default function LiveDashboard() \{
const \{ data, error, isLoading \} = useSWR(
"/api/metrics",
fetcher,
\{ refreshInterval: 5000 \} // poll every 5 seconds
);
if (isLoading) return <DashboardSkeleton />;
if (error) return <ErrorMessage error=\{error\} />;
return <MetricsDisplay data=\{data\} />;
\}Performance: Fastest initial shell (if combined with SSR shell), but content appears after JS loads and fetches complete.
When: A page has both static and dynamic content. The static shell should be instant, with dynamic parts streaming in.
Examples: Product pages (static description, dynamic price/stock), social feeds (static layout, dynamic content), dashboards (static nav, dynamic data).
// next.config.ts
const config = \{ experimental: \{ ppr: true \} \};
// app/product/[id]/page.tsx
import \{ Suspense \} from "react";
export default async function ProductPage(\{
params,
\}: \{
params: Promise<\{ id: string \}>;
\}) \{
const \{ id \} = await params;
const product = await getCachedProduct(id); // static, cached
return (
<main>
\{/* Static shell - prerendered at build time */\}
<h1>\{product.name\}</h1>
<p>\{product.description\}</p>
<StaticImages images=\{product.images\} />
\{/* Dynamic holes - streamed at request time */\}
<Suspense fallback=\{<PriceSkeleton />\}>
<LivePrice productId=\{id\} />
</Suspense>
<Suspense fallback=\{<StockSkeleton />\}>
<StockStatus productId=\{id\} />
</Suspense>
<Suspense fallback=\{<ReviewsSkeleton />\}>
<Reviews productId=\{id\} />
</Suspense>
</main>
);
\}Performance: Best of both worlds. Static shell served from CDN instantly, dynamic content streams in.
| Strategy | TTFB | FCP | SEO | Freshness | Server Cost |
|---|---|---|---|---|---|
| SSG | Fastest | Fastest | Excellent | Build-time only | None |
| ISR | Fast | Fast | Excellent | Periodic or on-demand | Low |
| SSR | Moderate | Moderate | Excellent | Real-time | High |
| CSR | Fast (shell) | Slow (content) | Poor (without SSR shell) | Real-time | Low (API only) |
| PPR | Fastest (shell) | Fast (shell) + streaming | Excellent | Hybrid | Moderate |
These functions and APIs make a route dynamic (SSR instead of SSG):
cookies() - Reading cookiesheaders() - Reading request headerssearchParams - Using search parametersfetch(url, { cache: "no-store" }) - Uncached fetchexport const dynamic = "force-dynamic" - Explicit opt-in
## Working Example
### Example 1: User asks "Should this page be SSR or SSG?"
**User prompt:** "I have a product detail page at /products/[slug]. The product info rarely changes but the price updates a few times a day."
**Skill-guided response:**
The product page should use **ISR with on-demand revalidation** for the product info, combined with **streaming** for the dynamic price:
```tsx
// app/products/[slug]/page.tsx
import { Suspense } from "react";
export async function generateStaticParams() {
const products = await getTopProducts(100); // Pre-build top 100
return products.map((p) => ({ slug: p.slug }));
}
export const revalidate = 86400; // Rebuild product info daily
export default async function ProductPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const product = await getProduct(slug);
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<Suspense fallback={<span>Loading price...</span>}>
<LivePrice slug={slug} />
</Suspense>
</main>
);
}
Plus a Server Action for on-demand revalidation when products are edited in the CMS.
Skill-guided response would include:
ppr: true in next.config.tsThis skill gives Claude a structured decision framework:
mkdir -p .claude/skills/nextjs-rendering
# Paste the Recipe content into .claude/skills/nextjs-rendering/SKILL.mdcookies() call in a deeply nested component forces the entire route to SSR. Use Suspense boundaries and PPR to isolate dynamic parts.experimental.ppr flag and may change in future releases.| Approach | When to Use |
|---|---|
| Astro | Content-heavy sites with minimal interactivity |
| Remix | Full SSR with nested data loading and mutations |
| Gatsby | Build-time static generation with GraphQL data layer |
| Nuxt.js | Vue.js equivalent with similar rendering strategies |
revalidateTag or revalidatePath triggered by a Server Action or webhookcookies() -- reading cookiesheaders() -- reading request headerssearchParams -- using search parametersfetch(url, { cache: "no-store" }) -- uncached fetchexport const dynamic = "force-dynamic" -- explicit opt-ingenerateStaticParams only pre-generates the params; the rendering strategy depends on what the component doesexperimental: { ppr: true } in next.config.tsexport default async function ProductPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// ...
}params is a Promise and must be awaitedPromise.all or streaming)cookies() or headers() when not needed, forcing the entire route to be dynamicrevalidate = 3600 on one page does not affect other pagesReviewed by Chris St. John·Last updated Jul 7, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥