Skip to content

New post: nextjs-ssr-in-practice #2

@CloudPower97

Description

@CloudPower97

Title

⚡ Server-Side Rendering (SSR) in Next.js: when to use it and how to do it right

Slug (kebab-case, without date)

nextjs-ssr-in-practice

Tags (stringa, space-separated)

nextjs ssr rendering

Image

No response

Body

> **Why it matters**: “SSR in Next.js” is no longer just “render HTML on every request.” With the App Router, you can mix **React Server Components (RSC)**, Client Components, streaming, a full-route cache, a data cache, and selective revalidation. Used well, you get excellent TTFB and SEO without melting your origin. Used poorly, you pay in cold starts, cache misses, and higher bills. This guide is a practical, production-oriented deep dive into **when SSR is the right tool**, how to wire it correctly in both the **App Router** and the **Pages Router**, and how to avoid the hidden traps. ([Next.js][1])

## Requirements

* **Next.js** with the **App Router** recommended (the Pages Router is still supported for legacy SSR via `getServerSideProps`). ([Next.js][2])
* **React 18+** for streaming SSR capabilities. ([legacy.reactjs.org][3])
* **Node.js runtime** by default; opt into **Edge** only if you need ultra-low latency and accept its API limitations (**no ISR support on Edge**). ([Next.js][4])

## What “SSR” means in Next.js (2025 edition)

Historically, “SSR” meant: the server generates HTML on every request. In the **Pages Router**, that’s still the definition: export `getServerSideProps` and the page is rendered for each request, typically without a server-side cache unless you add your own HTTP caching. It’s simple, but every hit triggers server work. ([Next.js][2])

In the **App Router**, SSR is more nuanced. **Layouts and pages are Server Components by default**; you can compose them with Client Components for interactivity. Rendering can be **static**, **dynamic (request-time)**, or **streaming**. Crucially, Next.js introduces **two caches**: the **Full Route Cache** (stores the rendered output) and the **Data Cache** (stores results of `fetch` calls). A route can be “dynamic” yet still hit the Data Cache for parts of its tree, keeping performance acceptable. ([Next.js][1])

### TL;DR

* **Pages Router:** SSR = render per request via `getServerSideProps`.
* **App Router:** SSR = server rendering with options: static, dynamic, streaming; caching is first-class and configurable at the route and fetch levels. ([Next.js][2])

## When to use SSR (and when not to)

Use **SSR** when at least one of these is true:

* You **must** show **personalized** content at request time (e.g., “Hello Emilia” or per-user pricing) where pre-rendering is impossible.
* The page depends on **rapidly changing data** (sub-minute) where **ISR** (time-based or on-demand) doesn’t fit your freshness guarantees.
* You need **SEO** and the **HTML must reflect live data** at the moment of the request (e.g., auction bids).

Prefer **static or ISR** when:

* Data is **stable** for seconds/minutes or can be **revalidated** by path or tag (admin updates content → trigger revalidation).
* You’re on **Edge runtime** (no ISR), unless you accept dynamic rendering for tiny handlers and understand the trade-offs. ([Next.js][5])

## Rendering modes in practice (App Router)

* **Static Rendering (SSG/ISR):** Next.js pre-renders and caches the output. Revalidate by time (`next: { revalidate }`) or explicitly with `revalidatePath` / `revalidateTag`. Great baseline. ([Next.js][5])
* **Dynamic Rendering (SSR):** Request-time rendering, typically triggered by using **dynamic APIs** like `cookies()`, `headers()`, `searchParams`, `draftMode()`, or by `fetch(..., { cache: 'no-store' })`; you can also **force** dynamics with `export const dynamic = 'force-dynamic'`. ([Next.js][6])
* **Streaming SSR:** Send the shell immediately and progressively stream in the rest (powered by React 18). Improves TTFB and UX. ([legacy.reactjs.org][3])
* **Partial Prerendering (PPR)** (experimental): Keep most of the route static and stream “live” islands into placeholders. Not recommended for production yet. ([Next.js][7])

## Decision framework (production)

1. **Static-first**: can this page be static with ISR? If yes, do that.
2. If freshness or personalization require request-time work, **scope** SSR to the smallest possible segment (don’t turn the whole page dynamic).
3. Use **tagged caching** (`revalidateTag`) to centralize invalidation signals. For predictable pages, use `revalidatePath`.
4. Default to **Node.js runtime**; consider **Edge** only for tiny, latency-critical handlers and where you don’t need ISR or Node-specific APIs. ([Next.js][8])

---

## SSR with the Pages Router (legacy but solid)

If you’re in the Pages Router, **SSR = `getServerSideProps`**. Next.js **pre-renders on every request**; you can pass props to the page component. It’s straightforward and predictable. Use HTTP caching at the CDN if you want to avoid fully dynamic behavior for anonymous traffic. ([Next.js][9])


// pages/news.tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';

export const getServerSideProps: GetServerSideProps = async () => {
  const res = await fetch('https://api.example.com/news');
  const data = await res.json();
  return { props: { data } };
};

export default function NewsPage(
  { data }: InferGetServerSidePropsType<typeof getServerSideProps>
) {
  return <ul>{data.items.map((it: any) => <li key={it.id}>{it.title}</li>)}</ul>;
}


> **Guideline:** If you don’t need per-request freshness, migrate to App Router and use static + ISR. Reserve Pages-SSR for legacy routes where a rewrite is costly. ([Next.js][2])

---

## SSR with the App Router: the real-world mechanics

In the App Router, **Server Components** are default. You decide if a route is static or dynamic by **what you do** in that route:

* If you call **dynamic functions** (`cookies()`, `headers()`, `draftMode()`) or use `searchParams` in `page.tsx/layout.tsx`, you opt into **dynamic rendering** for that route segment. ([Next.js][6])
* If your data fetching uses `fetch(..., { cache: 'no-store' })`, that **bypasses the Data Cache** and produces request-time behavior. ([Next.js][5])
* You can **force** either mode with the **Route Segment Config**: `export const dynamic = 'force-dynamic' | 'force-static' | 'auto'`. There are related knobs like `revalidate`, `fetchCache`, and `preferredRegion`. ([Next.js][10])

### Minimal SSR example (App Router)


// app/news/page.tsx
// Dynamic because we opt out of the Data Cache for this fetch:
export default async function Page() {
  const res = await fetch('https://api.example.com/news', { cache: 'no-store' });
  const data = await res.json();
  return (
    <ul>
      {data.items.map((it: any) => <li key={it.id}>{it.title}</li>)}
    </ul>
  );
}


That single `no-store` opt-in is enough to render on each request. If you later decide that minute-level freshness is fine, switch to:
`await fetch(url, { next: { revalidate: 60 } })`. ([Next.js][5])

### Mixing static + dynamic safely

A common mistake is flipping an entire route to dynamic when only a small “island” needs live data. Keep the **layout and most content static**, and **isolate the dynamic bit** into a small Server Component or a Route Handler. This preserves the Full Route Cache for most of the tree and minimizes server work. ([Next.js][11])

---

## Caching & revalidation you’ll actually use

**Core APIs:**

* **`fetch`**: Set `cache: 'no-store'` for request-time data, or `next: { revalidate: N }` for time-based ISR. You can **tag** fetches with `next: { tags: ['…'] }`. ([Next.js][5])
* **`revalidatePath('/path')`**: Invalidate by path, typically from a **Server Action** after a mutation. Note: historically this also impacted the client-side router cache more broadly; check your Next.js version notes. ([Next.js][12])
* **`revalidateTag('tag')`**: Invalidate **all** cache entries carrying this tag, across routes. Prefer tags when multiple pages consume the same data. ([Next.js][8])

**Example: tag-based invalidation after a write**


// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function createPost(data: FormData) {
  // ... persist
  revalidateTag('posts') // all routes that fetched with { next: { tags: ['posts'] } } update on next visit
}


This lets you keep most routes static while ensuring freshness after mutations. Users see updated content **on the next request** to affected paths. ([Next.js][13])

**Advanced knobs (use sparingly):**

* **Route Segment Config**: `export const dynamic = 'force-static' | 'force-dynamic'`, `export const revalidate = N`, `export const fetchCache = 'default-cache' | 'only-no-store' | …`. These are **sharp tools**—apply at the smallest segment possible. ([Next.js][10])
* **`use cache` directive**: An explicit way to pre-render and cache an entire route (layout+page) when you want to ensure static behavior. ([Next.js][14])

---

## Streaming SSR (and when it pays for itself)

React 18 unlocked **streaming server rendering** with Suspense boundaries. In Next.js, this means you can render the shell first and stream chunks of the RSC payload as data resolves. Streaming is a good fit for:

* Slow leaf data that can show a placeholder while the shell loads.
* SEO-critical pages where the head and skeleton must arrive fast.
  Just don’t stream everything—that defeats the goal. Start with a single Suspense boundary around the slowest component(s). ([legacy.reactjs.org][3])

---

## Partial Prerendering (PPR): promising but still experimental

PPR gives you the best of both worlds—static shell plus “live” holes streamed in one request—but it’s **experimental** and explicitly **not recommended for production** as of today. If you test it, do so behind a flag and be ready to revert. ([Next.js][7])

---

## Choosing the runtime (Node vs Edge)

* **Default: Node.js runtime**. Full Node APIs, compatible ecosystem, supports ISR.
* **Edge runtime**: Ultra-low latency, but **limited Node APIs** and **no ISR**. Some packages won’t work. Edge can still **stream** depending on deployment. Use Edge only for thin handlers (e.g., personalization header, geo routing) where you **accept** the constraints. ([Next.js][4])

You can set the runtime per route:
`export const runtime = 'nodejs' | 'edge'`. Keep Node unless you have a strong reason to switch. ([Next.js][15])

---

## SEO & SSR: the pragmatic view

Search engines are fine with CSR in many cases, but SSR gives you **reliable initial HTML** and faster first paint for crawlers and users on slow devices. In App Router, SSR doesn’t mean “everything dynamic”—you can keep most of the page static and only fetch what must be live. Combine **static metadata** with small dynamic islands for the bits that genuinely change per request. (Mechanics covered in the caching docs.) ([Next.js][16])

---

## Observability and debugging SSR

* **Know when a route is static or dynamic.** The “Static to Dynamic Error” doc explains why a page that was static at build time can become dynamic at runtime (often due to dynamic APIs introduced in code paths). Fix by eliminating the dynamic API or explicitly setting `dynamic = 'force-static'`. ([Next.js][17])
* **Dynamic APIs are async (Next 15).** You’ll see warnings if you use them synchronously; it’s a hint that you opted into dynamic rendering. Audit those call sites. ([Next.js][6])
* **Log `fetch` calls** in development to understand which requests hit the cache vs the network; the docs show how. ([Next.js][18])

---

## Cost control with SSR

SSR increases **server work per request**, so push as much as possible into **static generation + incremental updates**. When you must render at request time:

* **Cache aggressively** at the data level with `fetch` + tags; invalidate with `revalidateTag`.
* **Keep the dynamic surface small**: move personalization into the **last possible segment**, keep layouts static.
* **Stream** only the slow bits, don’t turn the entire tree into a waterfall. ([Next.js][16])

---

## Migration notes: Pages → App

* Replace `getServerSideProps` with **Server Components** that fetch on the server. Where possible, opt back into static rendering with revalidation for non-personalized sections.
* Use **Route Handlers** for RPC-style endpoints, and **Server Actions** for mutations; revalidate by path or tag post-write.
* Don’t forget to move Node-specific integrations to **Node runtime** routes if you’re tempted by Edge for latencies. ([Next.js][5])

---

## Patterns that scale

### 1) Static shell + dynamic widget

Keep `app/(marketing)/layout.tsx` static; mount a `<Cart/>` Server Component that reads `cookies()` and fetches live prices with `cache: 'no-store'`. Result: instant shell + precise dynamic island. ([Next.js][5])


// app/(shop)/cart/page.tsx
import { cookies } from 'next/headers'
export const revalidate = 300 // most of the page can stay static for 5 min

export default async function Page() {
  const cartId = cookies().get('cartId')?.value // makes this segment dynamic
  const res = await fetch(`https://api.example.com/cart/${cartId}`, { cache: 'no-store' })
  const { items } = await res.json()
  return <Cart items={items}/>
}


### 2) Tag-based consistency after mutations

Tag read fetches with `tags: ['posts']`. In a Server Action that creates/edits a post, call `revalidateTag('posts')`. All pages depending on those tags reflect new data on the next visit—without making everything dynamic. ([Next.js][8])


// reading
await fetch('https://api.example.com/posts', { next: { tags: ['posts'], revalidate: 3600 } })

// writing
'use server'
import { revalidateTag } from 'next/cache'
export async function publishPost() {
  // ... write
  revalidateTag('posts')
}


### 3) Route-level control with segment config

When a route unexpectedly flips dynamic (due to `cookies()` in a deep child), pin behavior explicitly:
`export const dynamic = 'force-static'` (or `'force-dynamic'` for truly per-request pages). Use `revalidate` at the segment for simple ISR. ([Next.js][10])

---

## Common gotchas (and fixes)

* **“Why did my page turn dynamic?”** You used `cookies()`, `headers()`, `draftMode()` or `searchParams` at the layout/page level, or you opted `no-store`. Either accept it or restructure so the dynamic API lives in a smaller segment. ([Next.js][6])
* **“My Edge route doesn’t regenerate.”** Edge **does not support ISR**. Move to Node runtime or redesign to use dynamic rendering plus data caching (no static regeneration). ([Next.js][4])
* **“I mixed Pages and App concepts.”** `getServerSideProps` is **Pages-only**; App Router uses Server Components + caching config. ([Next.js][2])
* **“Revalidation didn’t update immediately.”** `revalidatePath`/`revalidateTag` invalidate the cache; fresh data appears **on the next request** (unless you implement eager strategies where available). Design UX accordingly. ([Next.js][19])

---

## Minimal example (Pages Router SSR)


// pages/product/[id].tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const { id } = ctx.params as { id: string }
  const res = await fetch(`https://api.example.com/products/${id}`, {
    headers: { 'x-market': ctx.req.headers['x-market'] as string || 'it' }
  })
  const product = await res.json()
  return { props: { product } }
}

export default function ProductPage(
  { product }: InferGetServerSidePropsType<typeof getServerSideProps>
) {
  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.price} €</p>
    </main>
  )
}


**When to use**: per-request personalization or market-specific data where ISR isn’t acceptable. ([Next.js][9])

## Minimal example (App Router dynamic segment)


// app/product/[id]/page.tsx
export default async function Page({ params }: { params: { id: string } }) {
  const res = await fetch(`https://api.example.com/products/${params.id}`, {
    cache: 'no-store', // request-time
  })
  const product = await res.json()
  return (
    <main>
      <h1>{product.name}</h1>
      <p>{product.price} €</p>
    </main>
  )
}


**Variation**: If price updates hourly, replace `no-store` with `next: { revalidate: 3600, tags: ['products'] }` and revalidate after writes. ([Next.js][5])

---

## Pro Tip

**Don’t over-dynamic your app.** Start static, sprinkle dynamics only where value is real (auth, cart, live bids). If a single `cookies()` makes the whole segment dynamic, refactor the cookie read into a small Server Component and keep the rest cached. ([Next.js][6])

---

## Wrap-up

* **SSR is a scalpel, not a hammer.** In 2025, Next.js gives you a spectrum: static (with ISR), dynamic SSR, streaming SSR, and PPR (experimental).
* **Cache and revalidate intentionally.** The defaults help, but production systems need explicit `revalidatePath`/`revalidateTag` and careful use of `no-store`.
* **Choose Node runtime unless Edge provides real ROI**, and accept Edge constraints.
* **Keep dynamic work small.** Aim for static shells + dynamic islands.
  If you follow this, you’ll get predictable performance and costs while keeping the “live” feel your users expect. ([Next.js][5])

## Sources

* Next.js — **Server-Side Rendering (Pages)**: [https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering](https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering) ([Next.js][2])
* Next.js — **getServerSideProps**: [https://nextjs.org/docs/pages/api-reference/functions/get-server-side-props](https://nextjs.org/docs/pages/api-reference/functions/get-server-side-props) ([Next.js][9])
* Next.js — **Caching & Revalidating (App Router)**: [https://nextjs.org/docs/app/getting-started/caching-and-revalidating](https://nextjs.org/docs/app/getting-started/caching-and-revalidating) ([Next.js][5])
* Next.js — **Server & Client Components**: [https://nextjs.org/docs/app/getting-started/server-and-client-components](https://nextjs.org/docs/app/getting-started/server-and-client-components) ([Next.js][1])
* Next.js — **Route Segment Config**: [https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config) ([Next.js][10])
* Next.js — **Edge Runtime (caveats, no ISR)**: [https://nextjs.org/docs/app/api-reference/edge](https://nextjs.org/docs/app/api-reference/edge) ([Next.js][4])
* Next.js — **Fetching Data (App Router)**: [https://nextjs.org/docs/app/getting-started/fetching-data](https://nextjs.org/docs/app/getting-started/fetching-data) ([Next.js][18])
* React — **React 18 (streaming SSR)**: [https://legacy.reactjs.org/blog/2022/03/29/react-v18.html](https://legacy.reactjs.org/blog/2022/03/29/react-v18.html) ([legacy.reactjs.org][3])

*Last verified (Europe/Rome): 2025-09-26T18:00:00+02:00*

---

Quando sei pronto: apri il link dell’Issue Form, compila i campi (incolla il body qui sopra), **Submit** → la tua Action committerà il file in `_posts/YYYY-MM-DD-nextjs-ssr-in-practice.md` con `published: false` e `typewriter-delay: 20`.

[1]: https://nextjs.org/docs/app/getting-started/server-and-client-components?utm_source=chatgpt.com "Getting Started: Server and Client Components"
[2]: https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering?utm_source=chatgpt.com "Server-side Rendering (SSR)"
[3]: https://legacy.reactjs.org/blog/2022/03/29/react-v18.html?utm_source=chatgpt.com "React v18.0 – React Blog"
[4]: https://nextjs.org/docs/app/api-reference/edge?utm_source=chatgpt.com "API Reference: Edge Runtime"
[5]: https://nextjs.org/docs/app/getting-started/caching-and-revalidating?utm_source=chatgpt.com "Getting Started: Caching and Revalidating"
[6]: https://nextjs.org/docs/messages/sync-dynamic-apis?utm_source=chatgpt.com "Dynamic APIs are Asynchronous"
[7]: https://nextjs.org/docs/app/getting-started/partial-prerendering?utm_source=chatgpt.com "Getting Started: Partial Prerendering"
[8]: https://nextjs.org/docs/app/api-reference/functions/revalidateTag?utm_source=chatgpt.com "Functions: revalidateTag"
[9]: https://nextjs.org/docs/pages/api-reference/functions/get-server-side-props?utm_source=chatgpt.com "Functions: getServerSideProps"
[10]: https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config?utm_source=chatgpt.com "File-system conventions: Route Segment Config"
[11]: https://nextjs.org/docs/14/app/building-your-application/rendering/server-components?utm_source=chatgpt.com "Server Components - Rendering"
[12]: https://nextjs.org/docs/app/api-reference/functions/revalidatePath?utm_source=chatgpt.com "Functions: revalidatePath"
[13]: https://nextjs.org/docs/14/app/building-your-application/data-fetching/fetching-caching-and-revalidating?utm_source=chatgpt.com "Data Fetching, Caching, and Revalidating"
[14]: https://nextjs.org/docs/app/api-reference/directives/use-cache?utm_source=chatgpt.com "Directives: use cache"
[15]: https://nextjs.org/docs/14/app/building-your-application/rendering/edge-and-nodejs-runtimes?utm_source=chatgpt.com "Rendering: Edge and Node.js Runtimes"
[16]: https://nextjs.org/docs/app/guides/caching?utm_source=chatgpt.com "Guides: Caching"
[17]: https://nextjs.org/docs/messages/app-static-to-dynamic-error?utm_source=chatgpt.com "Resolving \"app/ Static to Dynamic Error\" in Next.js"
[18]: https://nextjs.org/docs/app/getting-started/fetching-data?utm_source=chatgpt.com "Getting Started: Fetching Data"
[19]: https://nextjs.org/docs/app/guides/incremental-static-regeneration?utm_source=chatgpt.com "How to implement Incremental Static Regeneration (ISR)"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions