Next.js App Router gives you three rendering strategies that you pick per route via exports at the top of page.tsx. Get this right and your site is fast + always-fresh; get it wrong and you’ll either burn server CPU or serve stale data.
Rendering overview
| Mode | When it renders | Best for |
|---|---|---|
| SSG (default) | Build time | Marketing pages, docs, blog posts that change infrequently |
| SSR | Every request | Personalized dashboards, real-time data, user-specific content |
| ISR | Build, then revalidated at intervals | News feeds, product listings, anything that changes hourly-ish |
| CSR (client) | Browser | Highly interactive widgets, after-auth views |
App Router defaults to SSG. You only opt out when you actually need SSR or ISR.
SSG — the default
Any page.tsx with no special exports is statically rendered at build time:
// app/about/page.tsx — renders once at `next build`
export default async function AboutPage() {
return <main>About me — never changes between deploys.</main>;
}
For dynamic routes with known slugs, implement generateStaticParams:
// app/blog/[slug]/page.tsx
import { getAllPosts, getPostBySlug } from '@/lib/posts';
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((p) => ({ slug: p.slug }));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}
SSR — force-dynamic
Opt into per-request rendering with a single export:
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const session = await getSession();
const data = await getUserData(session.userId);
return <Dashboard data={data} />;
}
Now every request to /dashboard runs the function and returns fresh HTML.
Alternative SSR signals
Next.js also infers force-dynamic when you use these inside a page:
cookies()fromnext/headersheaders()fromnext/headers- A
fetch()call withcache: 'no-store' - A search-param-dependent calculation
So you often don’t need the explicit export — just use a dynamic API and Next.js handles the rest.
ISR — revalidate
Combine SSG-style speed with periodic freshness:
// app/blog/[slug]/page.tsx
export const revalidate = 60; // seconds
export async function generateStaticParams() {
// ...same as SSG
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}
What happens:
- First request after build → serves the cached static HTML
- Background regeneration kicks off if cache is older than 60s
- Next request serves fresh HTML once regenerated
- Failed regeneration silently keeps serving stale (graceful degradation)
On-demand revalidation
For event-driven freshness (e.g., new blog post published), trigger revalidation explicitly:
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { secret, path } = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
}
revalidatePath(path);
return NextResponse.json({ revalidated: true, path });
}
Now your CMS can POST to /api/revalidate whenever content changes and the relevant page rebuilds within seconds.
Data fetching in App Router
The classic getServerSideProps / getStaticProps are gone in App Router. Just use async components with await fetch():
// app/posts/page.tsx
export default async function PostsPage() {
// SSG by default — fetched at build time
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return (
<ul>
{posts.map(p => <li key={p.id}>{p.title}</li>)}
</ul>
);
}
Control caching via the fetch options:
// SSR (no cache)
const data = await fetch('...', { cache: 'no-store' });
// ISR (revalidate every 60s)
const data = await fetch('...', { next: { revalidate: 60 } });
// SSG (default — cached forever until next build)
const data = await fetch('...');
// Tagged revalidation (invalidate by tag)
const data = await fetch('...', { next: { tags: ['posts'] } });
// later: revalidateTag('posts')
When to use what
Static content (about, docs, marketing) → SSG (default, no exports needed)
Logged-in dashboards, personalized views → SSR (`force-dynamic` or dynamic API)
Blog posts, product pages, news feeds → ISR (`revalidate: 60` to `3600`)
Real-time data (stock prices, chat) → CSR (client components + SWR/React Query)
Mixed content (mostly static, one dynamic widget) → SSG/ISR page + Client Component island
Best practices
- Default to SSG — opt out only when needed. Saved compute matters at scale.
- Co-locate revalidate with the data source — set
revalidatein thefetchcall rather than as a page export when only some data is fresh-sensitive. - Don’t mix
force-dynamicwithgenerateStaticParams— Next.js will warn; pick one. - Use
revalidateTagfor cross-route invalidation — when one DB write should refresh 5 routes, tag the fetches and invalidate once. - Edge runtime for pure-compute pages —
export const runtime = 'edge'cuts cold start to under 50ms for routes that don’t need Node APIs.
Common pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Build fails with “must implement generateStaticParams” | Dynamic route [slug] without generateStaticParams | Implement it, or set dynamic = 'force-dynamic' |
| Stale data after deploy | SSG cached at build time | Switch to ISR with revalidate, or use on-demand revalidatePath() |
| Server costs spiking | Accidental force-dynamic on a popular page | Audit page-level exports + fetch cache options |
cookies() errors at build | Using cookies inside an SSG page | Add export const dynamic = 'force-dynamic' |
| ISR not refreshing | Vercel deploys reset the ISR cache; or revalidate is in the future | Check next.config.js, verify timestamp |
Conclusion
Three rendering modes, one decision per route:
- SSG → fast, free, static-fresh data only
- SSR → expensive, fresh-every-time, user-specific data
- ISR → fast + periodically fresh, the sweet spot for most content sites
App Router makes the choice explicit and per-route. Use SSG by default; opt up to ISR for hourly-freshness; reach for SSR only when neither works.