Next.js Interview Prep
Rendering Strategies

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


Server-Side Rendering (SSR) thumbnail


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(), and headers()
  • 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  |
                                                              +-----------+

Server-Side Rendering (SSR) visual 1


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:

  1. The server sends complete HTML to the browser (the user can see the page)
  2. React's JavaScript loads in the browser
  3. React walks through the existing HTML and "claims" it — attaching event handlers, initializing useState, connecting useEffect, and wiring up all the interactive behavior
  4. 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:

ScenarioWhy SSR?
Personalized dashboardsEvery user sees different data based on their session
Authentication-gated pagesContent depends on cookies/tokens in the request
Real-time pricing / inventoryData must be accurate at the moment of the request
Search results pagesContent depends on query parameters unique to each request
Geo-targeted contentDifferent content based on request headers (location, language)
SEO-critical dynamic pagesContent changes often but must be crawlable (e-commerce product pages with live stock)

Don't Use SSR When:

ScenarioBetter Strategy
Blog posts, docs, marketing pagesSSG — content doesn't change per request
Product catalog that changes hourlyISR — revalidate periodically, not per-request
Highly interactive widgets (editors, maps)CSR — no server rendering benefit
Content that's identical for all usersSSG or ISR — no reason to re-render per request

Performance Implications of SSR

SSR has real costs that developers often overlook:

  1. 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.

  2. 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.

  3. 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.

  4. 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

ConceptKey Point
SSR is...Per-request rendering on the server — fresh HTML for every request
HTML qualityComplete, crawlable HTML (great for SEO)
When to usePersonalized, auth-gated, real-time, or request-dependent content
When NOT to useStatic content, same-for-everyone pages (use SSG/ISR instead)
Force SSRexport const dynamic = 'force-dynamic'
Auto SSR triggerscookies(), headers(), searchParams, fetch with cache: 'no-store'
HydrationReact attaches interactivity to server-rendered HTML
Hydration gapTime between "page visible" and "page interactive"
Main costHigher 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.

On this page