Next.js Rendering Strategies Skill - A Claude Code skill recipe for deep expertise in SSR, SSG, ISR, CSR, and PPR
These skill recipes are designed for Claude Code but also work with other AI coding agents that support skill/instruction files.
Recipe
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.
ISR - Incremental Static Regeneration
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.
SSR - Server-Side Rendering
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.
CSR - Client-Side Rendering
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.
PPR - Partial Prerendering
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.
Performance Trade-offs
| 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 |
Dynamic Detection
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
Common Pitfalls Per Strategy
SSG Pitfalls
- Forgetting generateStaticParams for dynamic routes (causes on-demand SSR)
- Build times grow linearly with page count (use partial generation)
ISR Pitfalls
- Setting revalidate too low (effectively becomes SSR)
- Not using tags for on-demand revalidation (relying only on time-based)
SSR Pitfalls
- Sequential data fetches creating waterfalls (use Promise.all or streaming)
- Not using Suspense to stream slow sections
- Accessing cookies/headers when not needed (forces entire route to be dynamic)
CSR Pitfalls
- No loading states (blank screen while JS loads)
- SEO-critical content rendered only client-side
- Ignoring error boundaries
PPR Pitfalls
- Suspense boundaries too granular (too many streaming holes)
- Suspense boundaries too coarse (large dynamic sections negate the static benefit)
- Not providing meaningful skeleton fallbacks
## 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.
Example 2: User asks "How do I set up PPR?"
Skill-guided response would include:
- Enable
ppr: truein next.config.ts - Identify which parts of each page are static vs dynamic
- Wrap each dynamic section in Suspense with a skeleton fallback
- Show complete page implementation with PPR boundaries
Deep Dive
How the Skill Works
This skill gives Claude a structured decision framework:
- Decision flowchart - Step-by-step logic to determine the right strategy
- Strategy reference - Complete implementation patterns for each rendering mode
- Performance trade-offs - Quantitative comparison to justify recommendations
- Pitfall awareness - Strategy-specific mistakes to warn about proactively
Customization
- Add your performance budgets (e.g., "TTFB must be under 200ms for all marketing pages")
- Specify default strategies per route group (e.g., "(marketing) = SSG, (app) = SSR")
- Include your monitoring/alerting conventions for rendering performance
How to Install
mkdir -p .claude/skills/nextjs-rendering
# Paste the Recipe content into .claude/skills/nextjs-rendering/SKILL.mdGotchas
- A single dynamic call makes the whole route dynamic - One
cookies()call in a deeply nested component forces the entire route to SSR. Use Suspense boundaries and PPR to isolate dynamic parts. - generateStaticParams does not guarantee SSG - If the page component uses dynamic APIs, the generated pages will still be SSR.
- ISR revalidation is per-page, not global - Each page has its own revalidation timer.
- PPR is experimental - As of Next.js 15, PPR requires the
experimental.pprflag and may change in future releases.
Alternatives
| 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 |
FAQs
What is the first question to ask when choosing a rendering strategy for a page?
- Does the content change per user or per request?
- If no, the page is a candidate for SSG or ISR
- If yes, consider whether the page has both static and dynamic parts (PPR) or is entirely dynamic (SSR/CSR)
When should you use ISR with on-demand revalidation instead of time-based ISR?
- When content changes are unpredictable and event-driven (e.g., CMS publish, admin update)
- Time-based ISR is better when content changes at a predictable interval
- On-demand ISR uses
revalidateTagorrevalidatePathtriggered by a Server Action or webhook
Which APIs and functions make a Next.js route dynamic?
cookies()-- reading cookiesheaders()-- reading request headerssearchParams-- using search parametersfetch(url, { cache: "no-store" })-- uncached fetchexport const dynamic = "force-dynamic"-- explicit opt-in
Gotcha: Does generateStaticParams guarantee that a page is statically generated?
- No. If the page component uses dynamic APIs (cookies, headers, uncached fetch), the generated pages will still be SSR
generateStaticParamsonly pre-generates the params; the rendering strategy depends on what the component does
What is Partial Prerendering (PPR) and when should you use it?
- PPR combines a static shell (prerendered at build time, served from CDN) with dynamic holes that stream in at request time
- Use it when a page has both static content (product description, layout) and dynamic content (price, stock, user-specific data)
- Requires
experimental: { ppr: true }in next.config.ts
How do you type the params prop for a dynamic route page in Next.js 15?
export default async function ProductPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// ...
}- In Next.js 15+,
paramsis aPromiseand must be awaited
What are the main SSR pitfalls listed on this page?
- Sequential data fetches creating waterfalls (use
Promise.allor streaming) - Not using Suspense to stream slow sections
- Accessing
cookies()orheaders()when not needed, forcing the entire route to be dynamic
Why is CSR a poor choice for SEO-critical content?
- CSR content only appears after JavaScript loads and the client-side fetch completes
- Search engine crawlers may not execute JavaScript or may see an empty shell
- Use SSR or SSG for any content that needs to be indexed by search engines
Gotcha: What happens if a single deeply nested component calls cookies()?
- The entire route becomes dynamic (SSR), not just that component
- To isolate dynamic behavior, use Suspense boundaries and PPR so only the dynamic part streams
What is the performance trade-off between SSG and SSR?
- SSG: fastest TTFB and FCP, served from CDN edge, zero server compute at request time
- SSR: moderate TTFB and FCP, every request hits the server, higher server cost
- SSG gives the best performance but only works for content that does not change per request
What are the common PPR pitfalls to watch for?
- Suspense boundaries too granular (too many streaming holes, overhead)
- Suspense boundaries too coarse (large dynamic sections negate the static benefit)
- Not providing meaningful skeleton fallbacks (poor UX during streaming)
How does ISR revalidation scope work -- is it global or per-page?
- ISR revalidation is per-page, not global
- Each page has its own independent revalidation timer
- Setting
revalidate = 3600on one page does not affect other pages