Server-Side Rendering
Trailhead's trail list can be filtered: /trails?difficulty=easy shows only easy trails, and the page greets a signed-in hiker by name. No one can prepare that page in advance, because it depends on the exact request. So the server builds it fresh, each time someone asks.
The rule
Needs the request? Then render per request, on the server.
With server-side rendering (SSR), also called dynamic rendering, the server runs your components when a request arrives, sends complete HTML, and the browser then hydrates it. Visitors see content immediately, and it can be specific to them.
In the App Router you rarely switch this on by hand. Next.js renders a route dynamically when it uses information that only exists at request time:
- the page's
searchParams(the query string), cookies()orheaders()fromnext/headers,- a
fetchthat opts out of caching with{ cache: "no-store" }.
// app/trails/page.tsx
import { cookies } from "next/headers";
import { getTrails } from "@/lib/trails";
export default async function TrailsPage({
searchParams,
}: {
searchParams: Promise<{ difficulty?: string }>;
}) {
const { difficulty } = await searchParams;
const hiker = (await cookies()).get("hiker")?.value;
const trails = await getTrails({ difficulty });
return (
<>
<p>Welcome back{hiker ? `, ${hiker}` : ""}</p>
<ul>{trails.map((t) => <li key={t.slug}>{t.name}</li>)}</ul>
</>
);
}Like params, searchParams and cookies() are asynchronous in current Next.js, so you await them.
You can also force it for a whole route with export const dynamic = "force-dynamic", but letting Next.js detect it is usually cleaner.
Costs: the server does work on every visit, and the response cannot start until the data it waits for is ready. A slow database makes every page view slow. Streaming, covered later, softens this.
The shortcut
Scan the page for request-time inputs: searchParams, cookies, headers, no-store fetches. Find one and the route is dynamic; find none and it can be prepared ahead of time.
Scan the props
It awaits
searchParamsfor the difficulty filter. That is a request-time input.
The trap is reading a cookie deep in a shared component "just for a greeting". It quietly makes every page that uses the component dynamic. Put request-specific bits where you mean them to be.
Drills
Answer fast — trust the rule.
What does the visitor's browser receive first from a server-rendered page?
Which of these makes a Next.js route render per request?
How do you read the query string in an App Router page?
What is the main cost of rendering a page per request?
Remember
- Needs the request? Render per request, on the server, sending finished HTML.
- Shortcut: scan for
searchParams,cookies,headersorno-storefetches. - Trap: a request-time read hidden in a shared component makes every page dynamic.
