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.
Next.js categorizes every route into one of two buckets. This decision happens automatically at build time.
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.
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
cookies(), headers(), or searchParams.{ 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.
It is vital to distinguish between Component Nature (Server/Client) and Rendering Timing (Static/Dynamic). They are independent axes.
| Component Type | Static (Build Time) | Dynamic (Request Time) |
|---|---|---|
| Server Component | Fetches DB data once at build. No JS sent to client. | Fetches DB data on every click. No JS sent to client. |
| Client Component | Pre-renders initial HTML at build. Hydrates in browser. | Pre-renders initial HTML on every request. Hydrates in browser. |
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>;
}| Feature | Static Rendering | Dynamic Rendering |
|---|---|---|
| When it happens | Build Time | Request Time |
| Data Freshness | Build-time only (until re-build) | Real-time |
| User Latency | Ultra Low (Instant from CDN) | Variable (Server must calculate) |
| Server Load | Low (Served as a file) | High (Server runs JS per visit) |
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.
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.
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
There are two ways to tell Next.js a route should be ISR
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>;
}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>;
}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.
cookies(), headers(), or user-specific DB queries inside a route using ISR.| Use Case | Strategy | Why? |
|---|---|---|
| Public Product Page | ISR | The product details are the same for everyone. |
| User Dashboard | Dynamic (SSR) | The data is unique to the userId in the cookie. |
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.
Imagine an E-commerce product page.
With PPR, Next.js generates a Static Shell at build time. When a user visits
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>
);
}| Strategy | Performance | Freshness | Best For |
|---|---|---|---|
| Static (SSG) | โกโก | โ Stale | Marketing, Docs |
| Dynamic (SSR) | ๐ข | โกโก | User Dashboards, Private data |
| ISR | โกโก | โ Background | Large Blogs, Product Catalogs |
| PPR | โก (Shell) | โ (Holes) | E-commerce, Social Feeds |
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.
When choosing a rendering strategy, follow this logic flow to ensure maximum performance
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.
Rendering strategies are Automatically Inferred by Next.js, but Guided by Developer signals.
npm run build, Next.js performs Static Analysis. If no dynamic functions (cookies, headers) or uncached fetches are found, it defaults to Static.revalidate timer, devs are opting-out of the static default.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.
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.
These are segments defined in our folder structure, like [id] or [slug].
These are query strings used for filtering or searching (e.g., /shop?sort=price).
searchParams is automatically opted-into Dynamic Rendering.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.
generateStaticParams FunctionThis 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(),
}));
}
If we have millions of IDs, we don't want to build them all at once (it would take hours).
generateStaticParams for the most popular 1,000 items.| Route Type | Mechanism | Result |
|---|---|---|
[id] folder | No extra config | Dynamic (Request-time) |
[id] folder | generateStaticParams | Static (Build-time) |
?query=... | searchParams prop | Dynamic (Always) |
We might be used to terms like SSG (Static Site Generation). In the App Router, we shift to Static Rendering.
Why the shift ?
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.
<Suspense>.| Feature | Prop Drilling (Old) | Streaming/PPR (New) |
|---|---|---|
| Initial Load | Waits for all data to finish. | Instant Static Shell (Header/Nav). |
| User Experience | Blank screen or global loader. | Interactive shell + specific component loaders. |
| DB Access | In Page or API Route. | Directly inside the async Server Component. |
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)?
We treat the Database as the single source of truth.
revalidatePath.revalidatePath tells Next.js to re-fetch all components on that page.We treat a Client Cache (like TanStack Query) as the global state.
useQuery + invalidateQueries.