Rendering Strategies

In Next.js, Rendering is no longer an all or nothing choice. The framework uses Automatic Rendering Optimization to decide how to deliver each route based on the features we use. The goal is to maximize performance by shifting as much work as possible to the Build Step.

Static vs Dynamic Rendering

Next.js categorizes every route into one of two buckets. This decision happens automatically at build time.

1. Static Rendering

By default, Next.js performs Static Rendering. This means the HTML and RSC Payload are generated once during the build process (when we run npm run build) or in the background.

  • The Benefit The output is stored on a Content Delivery Network (CDN). When a user visits, they receive a raw HTML file almost instantly from a server geographically close to them.
  • Use Case Marketing pages, blog posts, documentation, and product catalogs.
  • Architecture Since the data is fetched at build time, there is zero server think time when a user requests the page.

2. Dynamic Rendering

If Next.js detects that a route needs information it can only get at Request Time (when a specific user visits), it switches to Dynamic Rendering.

The Triggers

  • Dynamic Functions Using cookies(), headers(), or searchParams.
  • Uncached Data Using a fetch request with { cache 'no-store' }.

The Database Nuance Simply fetching from a database does NOT make a route dynamic. Next.js will attempt to fetch that data at build time and statically cache it. A database-driven route only becomes Dynamic if the query depends on a user's cookie, a header, or a live URL parameter.


The Rendering Matrix

It is vital to distinguish between Component Nature (Server/Client) and Rendering Timing (Static/Dynamic). They are independent axes.

Component TypeStatic (Build Time)Dynamic (Request Time)
Server ComponentFetches DB data once at build. No JS sent to client.Fetches DB data on every click. No JS sent to client.
Client ComponentPre-renders initial HTML at build. Hydrates in browser.Pre-renders initial HTML on every request. Hydrates in browser.

The Switch Mechanics

We don't usually set a page to be dynamic; Next.js infers it. However, we can force the behavior using Route Segment Config.

// Force a page to be dynamic even if it doesn't use cookies
export const dynamic = "force-dynamic";
 
export default async function Dashboard() {
  const data = await fetch("https//api.example.com/stats"); // No-cache is implied
  return <div>...</div>;
}

๐Ÿ“ Summary Performance Comparison

FeatureStatic RenderingDynamic Rendering
When it happensBuild TimeRequest Time
Data FreshnessBuild-time only (until re-build)Real-time
User LatencyUltra Low (Instant from CDN)Variable (Server must calculate)
Server LoadLow (Served as a file)High (Server runs JS per visit)

๐Ÿ›‘ Stop and Think

Next.js is a Static-First framework. Its primary goal is to move as much work as possible to the CDN. We should only move to Dynamic Rendering when we absolutely need access to user-specific data like session cookies or live search parameters.


Incremental Static Regeneration

Static rendering is fast, but it has a major flaw the data becomes stale the moment the build finishes. ISR is the Holy Grail of rendering that solves this. It allows us to update static pages after we've built our site, without needing a full redeploy.

1. The Strategy Stale While Revalidate

ISR works on a background refresh model. It doesn't make the user wait for the server to think. Instead, it serves a cached version while it fetches a fresh one in the background.

The Workflow

  1. Initial Build All pages are generated statically.
  2. User A Visits (Window of Validity) They get the cached version instantly.
  3. User B Visits (After Revalidate Period) They still\ see the stale (old) cached version (no waiting).
  • Next.js triggers a background re-render of that specific page.
  1. User C Visits (After Background Finish) They see the new, fresh version.

2. Implementation How to specify ISR

There are two ways to tell Next.js a route should be ISR

A. The Fetch Option

Inside Server Component, add the next.revalidate property to fetch call.

export default async function Page() {
  // This page is now ISR. It refreshes at most every 60 seconds.
  const res = await fetch(
    "[https//api.example.com/data](https//api.example.com/data)",
    {
      next { revalidate 60 },
    }
  );
  const data = await res.json();
 
  return <div>{/* ... */}</div>;
}

B. The Segment Config

If we are using a DB client (Prisma/Drizzle) instead of fetch, export the revalidate constant at the top of page.tsx.

// Revalidate this page every hour (3600 seconds)
export const revalidate = 3600;
 
import { db } from "@/lib/db";
 
export default async function Page() {
  const data = await db.posts.findMany();
  return <div>{/* ... */}</div>;
}

3. The ISR Constraint Shared vs. Private

A critical architectural rule ISR is for Shared Data only. Because the revalidated page is stored in a public cache, it must be independent of the user currently viewing it.

  • The Rule We cannot use cookies(), headers(), or user-specific DB queries inside a route using ISR.
  • The Risk If we try to fetch My Profile using ISR, the data of the user who triggered the revalidation will be cached and shown to every other user until the next revalidation cycle.
Use CaseStrategyWhy?
Public Product PageISRThe product details are the same for everyone.
User DashboardDynamic (SSR)The data is unique to the userId in the cookie.

Partial Prerendering

PPR is a new architectural pattern that eliminates the all or nothing choice between Static and Dynamic rendering. It allows a single route to have both static and dynamic parts.

1. The Static Shell Pattern

Imagine an E-commerce product page.

  • Static Parts Product title, description, and images (never change per user).
  • Dynamic Parts Personalized recommendations, stock levels, and shopping cart.

With PPR, Next.js generates a Static Shell at build time. When a user visits

  1. The Static Shell is sent immediately (instant load).
  2. The Dynamic Holes are filled in via Streaming as the server finishes the data fetching.

2. Leveraging React Suspense

PPR relies on Suspense boundaries to define where the Dynamic Holes are.

export default function ProductPage() {
  return (
    <main>
      <StaticProductInfo /> {/* Sent Instantly */}
      <Suspense fallback={<CartSkeleton />}>
        <DynamicCart /> {/* Streamed in later */}
      </Suspense>
    </main>
  );
}

๐Ÿ“ Rendering Strategy Cheat Sheet

StrategyPerformanceFreshnessBest For
Static (SSG)โšกโšกโŒ StaleMarketing, Docs
Dynamic (SSR)๐ŸขโšกโšกUser Dashboards, Private data
ISRโšกโšกโœ… BackgroundLarge Blogs, Product Catalogs
PPRโšก (Shell)โœ… (Holes)E-commerce, Social Feeds

๐Ÿ›‘ Stop and Think

ISR moves the 'Cost of Freshness' from the User to the Background Worker. The user always gets a fast experience, even if the data is a few minutes old. Only use Dynamic Rendering if the data must be 100% real-time for the user currently looking at the screen.


The Architect's Decision Logic

When choosing a rendering strategy, follow this logic flow to ensure maximum performance

  1. Is the data the same for every user ?
    • Yes Can it wait until the next build? -> Static (SSG).
    • Yes, but it changes often -> ISR (Stale-While-Revalidate).
  2. Is the data unique to the user ? (e.g., based on Cookies/Auth)
    • Yes -> Dynamic Rendering (SSR).

Common Mental Trap The Database

  • DB Fetching the Top 10 Movies from a database is Static/ISR.
  • DB + Cookie = Dynamic Fetching "Movies I Watched" from a database is Dynamic.

Note Always aim for the "highest" level in the Performance Pyramid (Static -> ISR -> Dynamic). Only move down a level if the requirements (User-specificity) force you to.


The Ownership of Decision

Rendering strategies are Automatically Inferred by Next.js, but Guided by Developer signals.

  1. Framework Role During npm run build, Next.js performs Static Analysis. If no dynamic functions (cookies, headers) or uncached fetches are found, it defaults to Static.
  2. Developer Role Devs provide Signals. By using a dynamic function or setting a revalidate timer, devs are opting-out of the static default.

The Signal Cheat Sheet

  • No dynamic signals -> Static.
  • Accessing Request Data -> Dynamic.
  • Adding a timer to fetch -> ISR.
  • Using Suspense + Experimental PPR -> PPR.

Here is the updated MDX content for sections 2.5 and 2.6. These additions clarify the distinction between route parameters and query strings, as well as the mechanism for turning dynamic URLs into static files.


The Params vs. SearchParams Distinction

Not all URL data is Dynamic. Next.js treats Route Parameters (parts of the path) and Query Parameters (after the ?) differently because of when they are known.

A. Route Params

These are segments defined in our folder structure, like [id] or [slug].

  • Rendering Static by default.
  • Logic Next.js assumes these represent a set of pages that can be known. Even though the ID changes, the template is stable.

B. Query Params

These are query strings used for filtering or searching (e.g., /shop?sort=price).

  • Rendering Dynamic.
  • Logic Because query strings are infinite, unpredictable, and can be appended to any URL at any time, any page using searchParams is automatically opted-into Dynamic Rendering.

Staticizing Dynamic Segments

A dynamic folder like [repoId] is technically a variable, but we can Staticize it to get CDN-level performance. This allows a URL like /repo/123 to be served as a pre-built static HTML file.

The generateStaticParams Function

This function runs at Build Time. It retrieves a list of data (e.g., our top 100 repositories) and tells Next.js Go ahead and build static HTML files for these specific IDs right now.

// app/repo/[id]/page.tsx
 
export async function generateStaticParams() {
  const repos = await fetch('[https//api.github.com/repos').then(res) => res.json());
 
  // Next.js builds a STATIC page for every ID in this list
  return repos.map((repo) => ({
    id repo.id.toString(),
  }));
}
 

The Infinite ID Solution

If we have millions of IDs, we don't want to build them all at once (it would take hours).

  1. Pre-render the Top Tier Use generateStaticParams for the most popular 1,000 items.
  2. On-Demand for the Rest When a user visits an ID that wasn't pre-rendered, Next.js generates it on the fly and caches it. The next user gets the static version.

Final Decision Matrix

Route TypeMechanismResult
[id] folderNo extra configDynamic (Request-time)
[id] foldergenerateStaticParamsStatic (Build-time)
?query=...searchParams propDynamic (Always)

Terminology: Generation vs. Rendering

We might be used to terms like SSG (Static Site Generation). In the App Router, we shift to Static Rendering.

Why the shift ?

  1. Granularity: We are no longer generating Sites. we are rendering Component Trees.
  2. Timing over Result: Generation implies a one-time file creation. Rendering describes the process of React turning our code into a UI, which can now happen at different times (Build, Request, or Background).

The New Vocabulary

  • CSR/SSR: Where the code runs (Client vs. Server).
  • Static/Dynamic: When the code runs (Build-time vs. Request-time).

The Data Fetching Pivot

In the traditional React world, we were taught to Lift State Up or fetch data at the top (Page level) and drill it down. In a Partial Prerendering (PPR) and Streaming architecture, we do the opposite: Fetch where you consume.

A. The Traditional Waterfall

  • The Parent Page fetches all data -> Passes it to children as props.
  • Result: The entire page is blocked. The user sees a blank screen or top loader until the slowest database query finishes.

B. The Streaming Pattern

  • The Parent renders Static components immediately (the Shell).
  • Dynamic components are wrapped in <Suspense>.
  • Database fetch happens INSIDE the child component.
FeatureProp Drilling (Old)Streaming/PPR (New)
Initial LoadWaits for all data to finish.Instant Static Shell (Header/Nav).
User ExperienceBlank screen or global loader.Interactive shell + specific component loaders.
DB AccessIn Page or API Route.Directly inside the async Server Component.

The Sync Dilemma: Do I need TanStack?

A common architectural question: If I fetch data in multiple Server Components, how do I keep them in sync when something changes (e.g., adding a Todo)?

A. The Server-First Sync

We treat the Database as the single source of truth.

  • Mechanism: Use Server Actions + revalidatePath.
  • Workflow: User submits a form -> Server Action updates DB -> revalidatePath tells Next.js to re-fetch all components on that page.
  • Benefit: All components (List, Counter, Sidebar) sync in one server round-trip. 0KB Client JavaScript.

B. The Client-First Sync

We treat a Client Cache (like TanStack Query) as the global state.

  • Mechanism: useQuery + invalidateQueries.
  • Workflow: Use this when you need Optimistic Updates (the UI reacts before the server confirms).