React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

skillsnextjsrenderingssrssgisrcsrpprserver-components

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

StrategyTTFBFCPSEOFreshnessServer Cost
SSGFastestFastestExcellentBuild-time onlyNone
ISRFastFastExcellentPeriodic or on-demandLow
SSRModerateModerateExcellentReal-timeHigh
CSRFast (shell)Slow (content)Poor (without SSR shell)Real-timeLow (API only)
PPRFastest (shell)Fast (shell) + streamingExcellentHybridModerate

Dynamic Detection

These functions and APIs make a route dynamic (SSR instead of SSG):

  • cookies() - Reading cookies
  • headers() - Reading request headers
  • searchParams - Using search parameters
  • fetch(url, { cache: "no-store" }) - Uncached fetch
  • export 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: true in 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:

  1. Decision flowchart - Step-by-step logic to determine the right strategy
  2. Strategy reference - Complete implementation patterns for each rendering mode
  3. Performance trade-offs - Quantitative comparison to justify recommendations
  4. 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.md

Gotchas

  • 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.ppr flag and may change in future releases.

Alternatives

ApproachWhen to Use
AstroContent-heavy sites with minimal interactivity
RemixFull SSR with nested data loading and mutations
GatsbyBuild-time static generation with GraphQL data layer
Nuxt.jsVue.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 revalidateTag or revalidatePath triggered by a Server Action or webhook
Which APIs and functions make a Next.js route dynamic?
  • cookies() -- reading cookies
  • headers() -- reading request headers
  • searchParams -- using search parameters
  • fetch(url, { cache: "no-store" }) -- uncached fetch
  • export 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
  • generateStaticParams only 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+, params is a Promise and must be awaited
What are the main SSR pitfalls listed on this page?
  • Sequential data fetches creating waterfalls (use Promise.all or streaming)
  • Not using Suspense to stream slow sections
  • Accessing cookies() or headers() 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 = 3600 on one page does not affect other pages