Skip to content

Server-Side Rendering (SSR)

Server-side rendering (SSR) generates a route's HTML fresh, on every single request, rather than reusing a build-time result. This lesson covers when and why Next.js renders a route this way.

Rendering Fresh HTML on Every Request

A route is server-side (dynamically) rendered whenever its content genuinely depends on something only known at request time — the logged-in user's session, request headers, real-time data that can't tolerate any staleness, or an explicit opt-out of caching. Next.js runs the route's Server Components again for every single incoming request.

In the App Router, there's no separate getServerSideProps function to write — you simply use APIs that inherently require per-request data (cookies(), headers(), or fetch(url, { cache: "no-store" })), and Next.js automatically renders that route dynamically.

// app/dashboard/page.tsx
import { cookies } from "next/headers";

export default async function DashboardPage() {
  const sessionId = cookies().get("session")?.value;
  const user = await getUserBySession(sessionId);

  return <h1>Welcome back, {user.name}</h1>;
}

Reading cookies() makes this page inherently request-specific, so Next.js renders it fresh for every visitor rather than reusing a cached build.

Forcing Dynamic Rendering Explicitly

// app/page.tsx
export const dynamic = "force-dynamic";

export default async function Page() {
  // always renders fresh, even without cookies/headers
}
  • export const dynamic = "force-dynamic" forces a route to render on every request, regardless of its data.
  • Reading cookies() or headers() implicitly makes a route dynamic without needing this export.
  • fetch(url, { cache: "no-store" }) also implicitly opts the route into dynamic rendering.
  • Dynamic rendering trades some performance for guaranteed per-request freshness.

SSR Cheat Sheet

What triggers dynamic rendering and its trade-offs.

Trigger Effect
cookies() / headers() Implicitly forces dynamic rendering
fetch(url, { cache: "no-store" }) Implicitly forces dynamic rendering
export const dynamic = "force-dynamic" Explicitly forces dynamic rendering
Rendering cost Runs on every request, not just once
Freshness Always current, never stale

SSR vs. Static Rendering vs. ISR

These three rendering strategies form a spectrum from "rendered once, ever" to "rendered on every request," with Incremental Static Regeneration (covered in the next lesson) sitting in between as a time-based compromise.

Strategy Rendered Freshness
Static Rendering Once, at build time Never updates without a rebuild
ISR Once, then periodically Updates on a timer or on demand
SSR (dynamic) Every request Always current

Performance Considerations for SSR

Because SSR runs your Server Components on every request, its response time is directly tied to how fast your data sources (databases, third-party APIs) respond. Streaming (covered in a later lesson) helps here by letting fast parts of the page render immediately while slower parts stream in afterward.

Common Mistakes

  • Using force-dynamic on every route out of caution, losing the performance benefits of static rendering and ISR.
  • Not realizing that reading cookies() even once, deep in the component tree, makes the whole route dynamic.
  • Assuming SSR in the App Router requires getServerSideProps — that's a Pages Router-only API.
  • Ignoring slow database queries in an SSR route, which directly slows down every single page load.

Key Takeaways

  • SSR renders a route's HTML fresh on every request, guaranteeing up-to-date content.
  • Reading cookies()/headers() or using cache: "no-store" implicitly triggers dynamic rendering.
  • export const dynamic = "force-dynamic" opts in explicitly, regardless of the data used.
  • SSR trades some raw performance for guaranteed freshness on every request.
  • Combine SSR with streaming for pages that mix fast and slow data sources.

Pro Tip

Before reaching for force-dynamic, check whether Incremental Static Regeneration (a periodic revalidation window) would satisfy your freshness requirements instead — many "needs to be live" requirements are actually fine with data that's at most a minute or two old, and ISR is significantly cheaper to run at scale.