intermediate Step 7 of 15

Server-Side Rendering (SSR)

Next.js Development

Server-Side Rendering (SSR)

Server-Side Rendering generates HTML on the server for every incoming request, producing a fresh page that reflects the current state of data and user context. Unlike SSG which serves the same pre-built page to all users, SSR can personalize content based on cookies, authentication tokens, query parameters, and headers. This makes SSR essential for pages that display user-specific data (dashboards, profiles, shopping carts), require real-time accuracy (stock prices, live scores), or need access to request-time information (geolocation, A/B testing). The trade-off is that SSR pages have higher Time to First Byte (TTFB) compared to static pages because the server must render HTML for each request.

SSR in the App Router

In the App Router, a page becomes server-rendered (dynamic) when it uses dynamic functions or opts out of caching. Dynamic functions include reading cookies, headers, or search params, and using cache: "no-store" on fetch requests.

// app/dashboard/page.tsx — SSR (dynamic rendering)
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

interface DashboardData {
  user: { name: string; email: string };
  stats: { projects: number; tasks: number; notifications: number };
  recentActivity: Array<{ id: number; action: string; date: string }>;
}

async function getDashboardData(token: string): Promise<DashboardData> {
  const res = await fetch("https://api.example.com/dashboard", {
    cache: "no-store", // Always fetch fresh data
    headers: { Authorization: `Bearer ${token}` },
  });

  if (!res.ok) throw new Error("Failed to fetch dashboard");
  return res.json();
}

export default async function DashboardPage() {
  const cookieStore = await cookies();
  const token = cookieStore.get("auth-token")?.value;

  if (!token) {
    redirect("/login");
  }

  const data = await getDashboardData(token);

  return (
    <div>
      <h1>Welcome back, {data.user.name}!</h1>
      <div className="stats-grid">
        <div className="stat-card">
          <h3>Projects</h3>
          <p>{data.stats.projects}</p>
        </div>
        <div className="stat-card">
          <h3>Tasks</h3>
          <p>{data.stats.tasks}</p>
        </div>
        <div className="stat-card">
          <h3>Notifications</h3>
          <p>{data.stats.notifications}</p>
        </div>
      </div>
      <h2>Recent Activity</h2>
      <ul>
        {data.recentActivity.map((item) => (
          <li key={item.id}>
            {item.action} — {new Date(item.date).toLocaleString()}
          </li>
        ))}
      </ul>
    </div>
  );
}

Dynamic Route Segments

You can explicitly control whether route segments are static or dynamic using segment configuration options.

// Force dynamic rendering for an entire route segment
export const dynamic = "force-dynamic"; // always SSR
// export const dynamic = "force-static";  // always SSG
// export const dynamic = "auto";          // let Next.js decide (default)

// Control revalidation for the entire page
export const revalidate = 0; // equivalent to no-store (SSR)
// export const revalidate = 60; // ISR every 60 seconds

// Example: search results page (always dynamic)
export const dynamic = "force-dynamic";

export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string; page?: string }>;
}) {
  const { q = "", page = "1" } = await searchParams;

  const results = await fetch(
    `https://api.example.com/search?q=${encodeURIComponent(q)}&page=${page}`,
    { cache: "no-store" }
  ).then((r) => r.json());

  return (
    <div>
      <h1>Search Results for "{q}"</h1>
      <p>{results.total} results found (page {page})</p>
      {results.items.map((item: any) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </div>
  );
}

Streaming SSR with Suspense

Next.js supports streaming SSR, which sends the page shell immediately and streams in slower-loading content as it becomes available. This dramatically improves perceived performance.

import { Suspense } from "react";

// Slow component that fetches heavy data
async function RecommendedPosts() {
  const posts = await fetch("https://api.example.com/recommendations", {
    cache: "no-store",
  }).then((r) => r.json());

  return (
    <section>
      <h2>Recommended for You</h2>
      {posts.map((post: any) => (
        <article key={post.id}>{post.title}</article>
      ))}
    </section>
  );
}

export default async function HomePage() {
  // This renders immediately
  return (
    <div>
      <h1>Home</h1>
      <p>Welcome to our platform!</p>

      {/* This streams in when ready */}
      <Suspense fallback={<div>Loading recommendations...</div>}>
        <RecommendedPosts />
      </Suspense>
    </div>
  );
}

Pages Router SSR (Legacy)

// pages/profile.tsx
export async function getServerSideProps(context) {
  const { req, res, params, query } = context;
  const token = req.cookies["auth-token"];

  if (!token) {
    return { redirect: { destination: "/login", permanent: false } };
  }

  const user = await fetch("https://api.example.com/me", {
    headers: { Authorization: `Bearer ${token}` },
  }).then((r) => r.json());

  // Cache for 60 seconds at the CDN level
  res.setHeader("Cache-Control", "s-maxage=60, stale-while-revalidate");

  return { props: { user } };
}

export default function ProfilePage({ user }) {
  return <h1>{user.name}'s Profile</h1>;
}
Tip: Use Streaming SSR with Suspense boundaries to send the fast-loading parts of the page immediately while slower data fetches complete in the background. This gives users something to look at right away instead of waiting for the entire page to render.

Key Takeaways

  • SSR renders fresh HTML per request, enabling personalized, real-time content.
  • Pages become dynamic when using cookies, headers, searchParams, or cache: "no-store".
  • Use export const dynamic = "force-dynamic" to explicitly force SSR for a page.
  • Streaming SSR with Suspense sends the page shell immediately and streams slow content.
  • SSR has higher TTFB than SSG — use it only when content must be fresh per request.