Every page in Next.js is rendered somewhere, at some time: per request on the server, once at build time, on a schedule, or in the browser. After this lesson you will be able to name the four strategies, tell which one the App Router picked for a route, and switch between them deliberately.
| Strategy | When HTML is produced | Best for |
|---|---|---|
| SSG (Static Site Generation) | Once, during next build | Marketing pages, docs, blog posts |
| ISR (Incremental Static Regeneration) | At build, then regenerated after a time window | Catalogs, news listings |
| SSR (Server-Side Rendering) | On the server for every request | Dashboards, personalised pages |
| CSR (Client-Side Rendering) | In the browser after JavaScript loads | Live widgets, browser-only data |
Each route decides for itself, and the App Router chooses automatically based on what your code does.
A route is statically rendered when its HTML can be produced without knowing anything about the incoming request. The result is written to disk during next build and served from a CDN.
// app/products/page.tsx
export default async function ProductsPage() {
const res = await fetch("https://api.example.com/products", { cache: "force-cache" });
const products: { id: string; name: string }[] = await res.json();
return <ul>{products.map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
}Since Next.js 15, fetch is not cached by default. cache: "force-cache" marks the response as reusable, so the page can be pre-rendered. Run next build and check the route table: static routes are marked ○ (Static).
Static pages go stale. ISR keeps static speed while refreshing periodically. Give the fetch a revalidate window, or set one for the whole route:
// per request
const res = await fetch("https://api.example.com/products", { next: { revalidate: 3600 } });
// or per route segment
export const revalidate = 3600; // secondsThe first request after 3600 seconds still receives the old HTML instantly; Next.js regenerates the page in the background and serves the fresh version to the next visitor.
A route becomes dynamically rendered the moment it touches request-time information. Any of these opt a route in automatically:
cookies(), headers() or the searchParams propfetch without caching (the default)connection() from next/serverexport const dynamic = "force-dynamic"// app/dashboard/page.tsx
import { cookies } from "next/headers";
export default async function Dashboard() {
const store = await cookies(); // request-time API => dynamic route
const theme = store.get("theme")?.value ?? "light";
return <p>Your theme is {theme}</p>;
}Dynamic routes show as ƒ (Dynamic) in the build output. cookies() and headers() are asynchronous and must be awaited.
Client Components are still server-rendered for their initial HTML, but data they load in useEffect or with a library such as SWR arrives in the browser after hydration. Use this for browser-only state or values that change every few seconds. Never rely on it for content search engines must index.
revalidate when data changes but not per user.cache: "force-cache" and wondering why a simple page is dynamic.cookies() in the root layout, which makes every route dynamic.useEffect, so crawlers see an empty shell.Which of these makes a route dynamically rendered in the App Router?
fetch is uncached by default; cache: "force-cache" or next: { revalidate } allows static rendering.cookies(), headers(), searchParams and force-dynamic opt a route into SSR.Next lesson: File-Based Routing with the App Router — see how folders and special files inside app/ become URLs.