One of the things that distinguishes Next.js from a plain React application is that it does not commit to a single rendering strategy. A plain React application renders everything in the browser — every page, every component, every data fetch happens on the client. Next.js offers a spectrum of choices, from pages rendered fresh on the server for every request, to pages built once at deploy time and served as static files indefinitely, to pages that sit somewhere between the two — pre-built, but periodically refreshed with new data.
These strategies are not marketing terms. They describe genuinely different tradeoffs between freshness, performance, and server load, and choosing the right one for each part of an application is one of the more consequential architectural decisions in Next.js development.
The Three Strategies
Before examining each in detail, it is worth naming them clearly and stating what each one means at the highest level.
Server-Side Rendering (SSR) — the page is rendered on the server for each individual request. Every time a user visits the URL, the server fetches the data, renders the page, and sends the result. The page is always up to date, because it is always rebuilt.
Static Site Generation (SSG) — the page is rendered at build time, before any user visits. The HTML is generated once, when the application is deployed, and served as a static file on every subsequent request. The page is extremely fast to serve, but it is frozen at the moment it was built.
Incremental Static Regeneration (ISR) — the page is generated statically at build time, but with a defined revalidation window. After that window expires, the next request triggers a background regeneration of the page. Users always receive a cached response; the cache is updated on a schedule rather than on every request or only at deploy time.
Each strategy is a response to a different set of requirements.
Server-Side Rendering — SSR
SSR produces a fresh HTML response for every request. The data is fetched during rendering, the component tree is rendered to HTML on the server, and the complete page is sent to the browser. By the time the user sees anything, the content is already in it.
In the App Router, a route renders server-side by default when its data fetches opt out of caching. The cache: 'no-store' option on a fetch call tells Next.js that this data must be fresh on every request, which in turn makes the entire route dynamic — rendered per request rather than pre-built.
// app/dashboard/page.tsx — rendered server-side on every request
export default async function DashboardPage() {
const data = await fetch('https://api.example.com/dashboard', {
cache: 'no-store', // no caching — always fresh
});
const dashboard = await data.json();
return (
<div>
<h1>Welcome back, {dashboard.user.name}</h1>
<p>You have {dashboard.notifications} unread notifications.</p>
</div>
);
}
This page shows data specific to the current user — notifications, personalised content, real-time values. It must be different for every user and current at the moment of the request. Caching it would mean serving one user's notifications to another, which is both incorrect and a security problem. SSR is the right strategy here.
When to Use SSR
SSR is appropriate when:
- The page content is specific to the individual user making the request — authentication state, personalised data, account information
- The data changes frequently enough that a cached version would be misleading or incorrect
- The page requires access to request-specific information — cookies, headers, the incoming URL — at render time
The cost of SSR is server load. Every request triggers a render, which may involve database queries and external API calls. For high-traffic routes, this cost compounds. SSR is the right tool for pages that genuinely need it; applying it universally when other strategies would serve equally well is a performance mistake.
Static Site Generation — SSG
SSG pre-renders pages at build time. When npm run build runs, Next.js generates HTML files for every static route and stores them. When users request those routes, Next.js serves the pre-built HTML directly — no rendering, no data fetching, no server processing. The response is as fast as serving a file.
A route is statically generated when its data fetches use caching and no dynamic behaviour is present. The cache: 'force-cache' option — or simply the absence of cache: 'no-store' — signals to Next.js that the data can be resolved at build time.
// app/about/page.tsx — statically generated at build time
export default async function AboutPage() {
const content = await fetch('https://api.example.com/about', {
cache: 'force-cache', // use cached value — resolved at build time
}).then(r => r.json());
return (
<article>
<h1>{content.title}</h1>
<div>{content.body}</div>
</article>
);
}
For dynamic routes, generateStaticParams tells Next.js which parameter values to pre-render. As covered in the Dynamic Routes topic, this function runs at build time and returns the set of pages to generate.
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetch(
`https://api.example.com/posts/${slug}`,
{ cache: 'force-cache' }
).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
At build time, Next.js calls generateStaticParams, retrieves every slug, and pre-renders an HTML file for each one. Deployment is a set of static files. Serving them requires no computation — the response is instant regardless of traffic volume.
When to Use SSG
SSG is appropriate when:
- The page content is the same for every user
- The content does not change between deployments, or changes infrequently enough that a rebuild is acceptable when it does
- Maximum performance is required — marketing pages, landing pages, documentation, blog content
The limitation of SSG is that the content is frozen at build time. A blog post published after the last deployment does not appear until the next build. For applications where content changes frequently and a rebuild cycle is impractical, SSG alone is insufficient. This is the problem ISR addresses.
Incremental Static Regeneration — ISR
ISR combines the performance of static pages with the freshness of server-side rendering. Pages are pre-built as static HTML, but they carry a revalidation window — a number of seconds after which the cached page is considered stale. When a request arrives for a stale page, Next.js serves the existing cached version immediately (so the user never waits) and triggers a background regeneration. Once the new version is ready, subsequent requests receive the updated content.
This is configured with the next: { revalidate } option on a fetch call:
// app/blog/[slug]/page.tsx — statically generated, revalidated every hour
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await fetch(
`https://api.example.com/posts/${slug}`,
{ next: { revalidate: 3600 } } // revalidate every 3600 seconds (1 hour)
).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
);
}
The page is generated at build time. For the first hour, every request receives the static HTML instantly. After an hour, the next request triggers a background regeneration — still receiving the cached page immediately — and the updated page is ready for the request after. The content stays reasonably fresh without requiring a rebuild or per-request rendering.
The revalidation window is a tradeoff. A short window — 60 seconds — means content is almost always current, at the cost of more frequent background regenerations. A long window — 86400 seconds, one day — means very little server work, at the cost of content that may be up to a day out of date. The right window depends on how often the content changes and how quickly those changes need to be visible.
On-Demand Revalidation
When a content update needs to be reflected immediately — a published post, a corrected error, a time-sensitive announcement — waiting for the revalidation window is not acceptable. On-demand revalidation using revalidatePath or revalidateTag, covered in the Data Fetching topic, purges the cache immediately and triggers a fresh generation on the next request. This gives ISR the ability to respond to specific events rather than only to time.
// Called from a webhook or admin action after content is updated
import { revalidatePath } from 'next/cache';
export async function POST() {
revalidatePath('/blog');
return Response.json({ revalidated: true });
}
When to Use ISR
ISR is appropriate when:
- The content changes periodically but not on every request
- The content is the same for all users (no personalisation)
- Near-real-time freshness is needed but per-request rendering is too expensive
- A content management system or external API is the source of truth, and updates happen through editorial actions rather than user interactions
Blog content, product catalogues, news articles, and documentation that is updated regularly but not continuously are all natural fits for ISR.
How Next.js Determines the Rendering Strategy
In the App Router, the rendering strategy is not declared explicitly — it is inferred from the code. Next.js analyses each route at build time and determines whether it can be pre-rendered or must be rendered dynamically.
A route is static (SSG or ISR) when:
- All
fetchcalls use caching (force-cacheornext: { revalidate }) - No dynamic functions are called at render time —
cookies(),headers(),searchParams generateStaticParamsprovides the full set of params for dynamic routes
A route is dynamic (SSR) when:
- Any
fetchcall usescache: 'no-store' - Dynamic functions are called —
cookies(),headers(), orsearchParamsare read - The route has dynamic segments and
generateStaticParamsis not provided
// This makes the entire route dynamic
import { cookies } from 'next/headers';
export default async function ProfilePage() {
const cookieStore = await cookies();
const token = cookieStore.get('auth-token');
// Reading cookies at render time — this route will be SSR
}
This inference can be made explicit with route segment configuration options:
// Force the route to be dynamic regardless of other signals
export const dynamic = 'force-dynamic';
// Force the route to be static — will error if dynamic behaviour is detected
export const dynamic = 'force-static';
Rendering Strategies Compared
| SSG | ISR | SSR | |
|---|---|---|---|
| When rendered | Build time | Build time, then periodically | Every request |
| Data freshness | Frozen at build | Refreshed on schedule or on demand | Always current |
| Response speed | Instant — static file | Instant — cached file | Render time — varies |
| Server load | None at request time | Minimal — background regeneration | Per request |
| Personalisation | Not possible | Not possible | Fully possible |
| Best for | Rarely-changing content | Periodically-changing content | User-specific, real-time content |
Mixing Strategies in One Application
One of the most important things to understand about Next.js rendering strategies is that they apply per route, not per application. A single Next.js application can have static marketing pages, ISR blog posts, and server-rendered dashboard pages — each route using the strategy that fits its requirements.
app/
├── page.tsx ← SSG — marketing homepage, rarely changes
├── about/
│ └── page.tsx ← SSG — static content
├── blog/
│ └── [slug]/
│ └── page.tsx ← ISR — content updated periodically
└── dashboard/
└── page.tsx ← SSR — user-specific, must be fresh
This is the pragmatic strength of Next.js: the rendering strategy is a per-route decision driven by the nature of the content, not a global architectural commitment. The framework handles the rest.