Server-Side Rendering (SSR)
Every Request Gets a Freshly Cooked Page
LinkedIn Hook
"Our dashboard loaded in 4.2 seconds. Users were bouncing before they saw a single chart."
We switched one line of code and brought it down to 1.1 seconds — without touching the database, the API, or the frontend logic.
The fix? Understanding when Server-Side Rendering is the right strategy and when it's actively hurting you.
SSR isn't a magic bullet. It's a trade-off — and most developers get the trade-off wrong because they don't understand what actually happens between the user's click and the first painted pixel.
In Lesson 2.1, you'll learn the full SSR lifecycle in Next.js App Router, when SSR is the best choice, when it's the worst, and how to force it with
dynamic = 'force-dynamic'.Read the full lesson -> [link]
#NextJS #SSR #ServerSideRendering #WebPerformance #FrontendDevelopment #InterviewPrep #React
What You'll Learn
- What SSR actually is and how it differs from other rendering strategies in Next.js
- The complete SSR lifecycle: request -> server render -> HTML response -> hydration
- How to force SSR in the App Router using
dynamic = 'force-dynamic',cookies(), andheaders() - When SSR is the right choice (and when it's actively slowing your app down)
- What hydration is, why it exists, and what happens when it fails
The Restaurant Analogy — Made to Order
Imagine three types of restaurants:
Static Site Generation (SSG) is a buffet. All the food is prepared before any customer arrives. You walk in, grab a plate, and eat immediately. It's fast, but everyone gets the same menu — and if the chef wants to change a dish, they have to redo the entire buffet.
Client-Side Rendering (CSR) is a meal kit delivery. You receive raw ingredients and a recipe at your door. You do all the cooking yourself, in your own kitchen (the browser). You get exactly what you want, but you're staring at an empty plate until the cooking is done.
Server-Side Rendering (SSR) is a made-to-order restaurant. You sit down, the waiter takes your specific order (the request), the kitchen (the server) cooks your personalized meal from scratch, and delivers it hot and ready. Every customer gets a freshly prepared dish tailored to their order — but they have to wait for the kitchen to cook it.
That waiting time is the core trade-off of SSR. You get fresh, personalized content on every request, but the server must do the work before the user sees anything.
How SSR Works — The Full Lifecycle
When a user requests an SSR page in Next.js, here is exactly what happens, step by step:
Step 1: The Browser Sends a Request
The user types a URL or clicks a link. The browser sends an HTTP GET request to the Next.js server.
Step 2: The Server Executes Your Component
The Next.js server receives the request. It runs your React Server Component on the server, including any data fetching (database queries, API calls, reading cookies). This happens on every single request — not once at build time.
Step 3: The Server Renders HTML
React takes the component output and renders it into a complete HTML string. This is real, readable HTML — not an empty <div id="root"> like you get with CSR.
Step 4: The Server Sends the HTML Response
The fully rendered HTML is sent to the browser. The user sees content immediately — even before any JavaScript loads. This is why SSR is excellent for SEO: crawlers get complete HTML.
Step 5: The Browser Downloads JavaScript
While the user is already reading the content, the browser downloads the React JavaScript bundle in the background.
Step 6: Hydration
React "hydrates" the static HTML — it attaches event listeners, initializes state, and makes the page interactive. The page goes from "looks like a website" to "works like a website."
+--------+ +-----------+ +------------------+ +---------+
| Browser| ----> | Next.js | ----> | React renders | ----> | Browser |
| sends | | server | | component to | | receives|
| request| | runs | | HTML string | | full |
| | | component | | | | HTML |
+--------+ +-----------+ +------------------+ +----+----+
|
v
+-----------+
| User sees |
| content |
| instantly |
+-----+-----+
|
v
+-----------+
| JS loads |
| + hydrate |
| = inter- |
| active |
+-----------+
SSR in the Next.js App Router — Code Examples
In Next.js 14+ with the App Router, all components are Server Components by default. But "Server Component" doesn't automatically mean "SSR on every request." Next.js is smart — it tries to statically render pages at build time when possible (which is SSG, not SSR).
To force a page to render on every request (true SSR), you need to tell Next.js that the page depends on dynamic, request-time data.
Method 1: Using dynamic = 'force-dynamic'
The most explicit way to opt into SSR:
// app/dashboard/page.tsx
// This tells Next.js: "Never cache this page. Render it fresh on every request."
export const dynamic = 'force-dynamic';
async function getDashboardData(userId: string) {
// This runs on the server, on EVERY request
const res = await fetch(`https://api.example.com/dashboard/${userId}`, {
// no-store ensures fetch itself is not cached either
cache: 'no-store',
});
return res.json();
}
export default async function DashboardPage() {
// In a real app, you'd get userId from auth/session
const data = await getDashboardData('user-123');
return (
<div>
<h1>Welcome back, {data.name}</h1>
<p>Your balance: ${data.balance}</p>
<p>Last login: {data.lastLogin}</p>
{/* This HTML is generated fresh on the server for each request */}
</div>
);
}
// Output (HTML sent to browser on each request):
// <div>
// <h1>Welcome back, Alice</h1>
// <p>Your balance: $2,847.50</p>
// <p>Last login: 2025-04-11T10:30:00Z</p>
// </div>
Method 2: Using Dynamic Functions (cookies(), headers())
When you call certain functions that depend on the incoming request, Next.js automatically switches to SSR — no force-dynamic needed:
// app/profile/page.tsx
import { cookies, headers } from 'next/headers';
export default async function ProfilePage() {
// Calling cookies() makes this page dynamic automatically
// Next.js detects: "This page reads cookies — it MUST render per-request"
const cookieStore = await cookies();
const theme = cookieStore.get('theme')?.value || 'dark';
const authToken = cookieStore.get('auth-token')?.value;
// Calling headers() also triggers dynamic rendering
const headersList = await headers();
const userAgent = headersList.get('user-agent') || 'Unknown';
const acceptLanguage = headersList.get('accept-language') || 'en';
if (!authToken) {
return <div>Please log in to view your profile.</div>;
}
// Fetch user data using the auth token from the cookie
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${authToken}` },
cache: 'no-store',
});
const user = await res.json();
return (
<div data-theme={theme}>
<h1>{user.name}'s Profile</h1>
<p>Email: {user.email}</p>
<p>Browser: {userAgent.includes('Chrome') ? 'Chrome' : 'Other'}</p>
<p>Language preference: {acceptLanguage}</p>
</div>
);
}
// This page is SSR because:
// 1. cookies() reads request-specific data
// 2. headers() reads request-specific data
// Next.js cannot pre-render this at build time — every user has different cookies
Method 3: Using searchParams
Accessing search parameters also forces dynamic rendering:
// app/search/page.tsx
// searchParams is a dynamic API — Next.js renders this on every request
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string }>;
}) {
const { q = '', page = '1' } = await searchParams;
const res = await fetch(
`https://api.example.com/search?q=${q}&page=${page}`,
{ cache: 'no-store' }
);
const results = await res.json();
return (
<div>
<h1>Results for "{q}"</h1>
<p>Page {page} of {results.totalPages}</p>
<ul>
{results.items.map((item: { id: string; title: string }) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
// URL: /search?q=nextjs&page=2
// Output: Fresh results rendered on the server for each unique search
Understanding Hydration — The Bridge Between Server HTML and Client Interactivity
Hydration is one of the most commonly misunderstood concepts in SSR, and interviewers love asking about it.
Here is what hydration actually does:
- The server sends complete HTML to the browser (the user can see the page)
- React's JavaScript loads in the browser
- React walks through the existing HTML and "claims" it — attaching event handlers, initializing
useState, connectinguseEffect, and wiring up all the interactive behavior - The page becomes fully interactive
Before hydration: The user sees a rendered page but buttons don't work, forms don't submit, and no JavaScript logic runs. It's like a photograph of a website.
After hydration: The page is a living, interactive React application.
// app/counter/page.tsx
// This page needs a Client Component for interactivity
import Counter from './Counter';
// Server Component — renders HTML on the server
export default async function CounterPage() {
const res = await fetch('https://api.example.com/initial-count', {
cache: 'no-store',
});
const { count } = await res.json();
return (
<div>
<h1>Server-rendered heading (no hydration needed for this)</h1>
{/* Counter is a Client Component — it will be hydrated */}
<Counter initialCount={count} />
</div>
);
}
// app/counter/Counter.tsx
'use client'; // This component needs hydration for interactivity
import { useState } from 'react';
export default function Counter({ initialCount }: { initialCount: number }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>Count: {count}</p>
{/* This button does NOTHING until hydration completes */}
{/* The server sends the HTML <button>+1</button> */}
{/* But the onClick only works after React hydrates this component */}
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
// Timeline:
// 0ms — User requests page
// 200ms — Server returns HTML (user sees "Count: 5" and a "+1" button)
// 200ms — User CAN see the button but clicking does nothing yet
// 800ms — JavaScript loads and React hydrates the component
// 800ms+ — Clicking "+1" now works, count increments to 6
The Hydration Gap
The time between "page is visible" and "page is interactive" is called the hydration gap (or sometimes Time to Interactive - TTI). This is a critical performance metric. A large hydration gap means users see buttons they can't click — which feels broken.
When to Use SSR — The Decision Framework
SSR shines in specific scenarios. Using it everywhere is a mistake.
Use SSR When:
| Scenario | Why SSR? |
|---|---|
| Personalized dashboards | Every user sees different data based on their session |
| Authentication-gated pages | Content depends on cookies/tokens in the request |
| Real-time pricing / inventory | Data must be accurate at the moment of the request |
| Search results pages | Content depends on query parameters unique to each request |
| Geo-targeted content | Different content based on request headers (location, language) |
| SEO-critical dynamic pages | Content changes often but must be crawlable (e-commerce product pages with live stock) |
Don't Use SSR When:
| Scenario | Better Strategy |
|---|---|
| Blog posts, docs, marketing pages | SSG — content doesn't change per request |
| Product catalog that changes hourly | ISR — revalidate periodically, not per-request |
| Highly interactive widgets (editors, maps) | CSR — no server rendering benefit |
| Content that's identical for all users | SSG or ISR — no reason to re-render per request |
Performance Implications of SSR
SSR has real costs that developers often overlook:
-
Time to First Byte (TTFB) increases — The server must fetch data AND render HTML before sending anything. With SSG, the HTML is pre-built and served instantly from a CDN.
-
Server load scales with traffic — Every request triggers a full render cycle. 10,000 simultaneous users means 10,000 simultaneous server renders. SSG serves the same pre-built file to all 10,000 users with zero computation.
-
No CDN caching (by default) — SSR responses are unique per request, so CDNs can't cache them without explicit configuration. SSG pages live on the CDN edge by default.
-
Cold starts on serverless — If you're deployed on serverless (Vercel, AWS Lambda), the first request after idle may have additional latency as the function spins up.
Performance comparison (conceptual):
Request → Response time:
SSG: [====] 50ms (serve pre-built file from CDN)
ISR: [====] 50ms (serve cached, revalidate in background)
SSR: [================] 200ms (fetch data + render on server)
CSR: [==] 30ms HTML + [==============] 500ms JS + render
SSR TTFB is higher, but:
- User sees REAL content at 200ms (not a loading spinner)
- Search engines see REAL content (not empty HTML)
- No layout shift from loading states
The dynamic Export Option — Full Reference
Next.js provides a route segment config option called dynamic that controls the rendering behavior:
// Force static rendering (SSG) — error if dynamic functions are used
export const dynamic = 'force-static';
// Automatic — Next.js decides based on your code (default)
export const dynamic = 'auto';
// Force dynamic rendering (SSR) — render on every request
export const dynamic = 'force-dynamic';
// Error if dynamic functions are used (stricter than force-static)
export const dynamic = 'error';
Here is what each option does in practice:
// Example: Same page, different dynamic options
// With force-static — this would ERROR because cookies() is dynamic
export const dynamic = 'force-static';
// cookies(); // ERROR: Dynamic server usage
// With auto — Next.js sees cookies() and automatically uses SSR
export const dynamic = 'auto';
// cookies(); // Works — Next.js switches to dynamic rendering
// With force-dynamic — always SSR, even if no dynamic functions
export const dynamic = 'force-dynamic';
// Even a page with zero dynamic functions will render per-request
Key insight for interviews: In the App Router, you rarely need to explicitly set dynamic = 'force-dynamic'. If you use cookies(), headers(), searchParams, or fetch() with cache: 'no-store', Next.js automatically opts into dynamic rendering. The force-dynamic option exists for cases where you want SSR even when your code doesn't use these APIs — for example, when you need to ensure a page is never cached.
Common Mistakes
-
Using SSR for content that doesn't change per request. If your "About Us" page uses
force-dynamic, you're making the server re-render identical HTML on every request. That's wasted computation and slower TTFB for zero benefit. Use SSG for static content. -
Confusing Server Components with SSR. Server Components run on the server, but that doesn't mean they run on every request. A Server Component can be statically rendered at build time (SSG). "Server Component" describes where the code runs. "SSR" describes when it runs (per request). They're orthogonal concepts.
-
Forgetting about the hydration gap. Developers test SSR pages on fast local machines and miss the fact that on a slow 3G connection, users might stare at an unresponsive page for several seconds. Interactive elements should have visual indicators (disabled states, loading hints) until hydration completes.
-
Not considering server costs at scale. SSR means your server does work on every request. For a page with 1 million daily visits, that's 1 million server-side renders. SSG serves a cached file — essentially free. Always ask: "Does this page truly need to be different for every request?"
Interview Questions
1. What is Server-Side Rendering, and how does it differ from Static Site Generation?
(Covered in the Restaurant Analogy and When to Use SSR sections above.)
2. Walk me through what happens from the moment a user requests an SSR page to when the page becomes interactive.
(Covered in the Full Lifecycle and Hydration sections above.)
3. In Next.js App Router, how do you force a page to use SSR? Name at least two methods.
(Covered in the Code Examples section — dynamic = 'force-dynamic', using cookies(), headers(), or searchParams.)
4. What is hydration, and what is the "hydration gap"? Why does it matter for user experience?
(Covered in the Hydration section above.)
5. Your team has a product listing page that shows the same products to all users but updates inventory counts every few minutes. A junior developer set it up with dynamic = 'force-dynamic'. What would you recommend instead, and why?
ISR (Incremental Static Regeneration) would be a better choice. Since the content is identical for all users and only changes every few minutes, there's no reason to re-render on every request. With ISR, you'd set
export const revalidate = 60(or similar), and Next.js would serve a cached static page to all users while periodically regenerating it in the background when the data becomes stale. This eliminates per-request server load, enables CDN caching, and reduces TTFB — while still keeping the data reasonably fresh. SSR should be reserved for truly personalized or request-dependent content.
Quick Reference -- Cheat Sheet
| Concept | Key Point |
|---|---|
| SSR is... | Per-request rendering on the server — fresh HTML for every request |
| HTML quality | Complete, crawlable HTML (great for SEO) |
| When to use | Personalized, auth-gated, real-time, or request-dependent content |
| When NOT to use | Static content, same-for-everyone pages (use SSG/ISR instead) |
| Force SSR | export const dynamic = 'force-dynamic' |
| Auto SSR triggers | cookies(), headers(), searchParams, fetch with cache: 'no-store' |
| Hydration | React attaches interactivity to server-rendered HTML |
| Hydration gap | Time between "page visible" and "page interactive" |
| Main cost | Higher TTFB, server load scales with traffic, no CDN caching by default |
+-----------------------------------------------+
| SSR Mental Model |
+-----------------------------------------------+
| |
| 1. User requests page |
| 2. Server fetches data (DB, API, cookies) |
| 3. Server renders React -> HTML |
| 4. Server sends complete HTML to browser |
| 5. User SEES content (not interactive yet) |
| 6. JavaScript loads |
| 7. React hydrates -> page is interactive |
| |
| Key trade-off: |
| + Fresh, personalized, SEO-friendly |
| - Slower TTFB, server cost per request |
| |
| Decision rule: |
| "Does every user need DIFFERENT content?" |
| Yes -> SSR |
| No -> SSG or ISR |
| |
+-----------------------------------------------+
Previous: Lesson 1.4 -- Next.js Compilation & Bundling Next: Lesson 2.2 -- Static Site Generation (SSG) ->
This is Lesson 2.1 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.