Next.js Interview Prep
Rendering Strategies

Static Site Generation (SSG)

Pages Built Before Anyone Asks

LinkedIn Hook

Your Next.js page takes 3 seconds to load.

You add a database query, a CMS fetch, and some markdown parsing. Now it takes 5 seconds.

But here's the thing — that content hasn't changed in 3 weeks.

Why is your server doing the same work for every single visitor?

Static Site Generation (SSG) builds your pages once at deploy time and serves them as plain HTML files. No server computation per request. No database calls per visitor. Just instant delivery from a CDN edge node 50ms from your user.

SSG is the fastest rendering strategy in Next.js — and interviewers love asking when to use it, how generateStaticParams works, and where SSG breaks down.

In this lesson, I break down SSG from the ground up — how build-time rendering works, when it's the right choice, how to generate dynamic routes statically, and how it connects to ISR.

If you've ever deployed a blog that felt slower than it should — this one's for you.

Read the full lesson -> [link]

#NextJS #SSG #StaticSiteGeneration #WebPerformance #InterviewPrep #Frontend #React #CodingInterview #100DaysOfCode


Static Site Generation (SSG) thumbnail


What You'll Learn

  • What Static Site Generation is and how it differs from SSR
  • How Next.js generates pages at build time and serves them as static HTML
  • When SSG is the ideal strategy (and when it absolutely is not)
  • How generateStaticParams pre-builds dynamic routes like /blog/[slug]
  • Why SSG is the fastest rendering strategy and how CDNs amplify that speed
  • The connection between SSG and Incremental Static Regeneration (ISR)

The Concept — What Is Static Site Generation?

Analogy: The Newspaper Printing Press

Think about how a newspaper works.

Every morning at 4 AM, the printing press runs. It takes the articles, the headlines, the photos — everything the editors finalized the night before — and prints thousands of identical copies. When you buy a newspaper at 7 AM, nobody is writing your copy on the spot. It was already printed hours ago. The vendor just hands you a pre-made copy.

SSG is the printing press. When you run next build, Next.js "prints" every page as a static HTML file. When a user visits your site, the server (or CDN) doesn't compute anything — it just hands over the pre-printed page. Every visitor gets the same pre-built file, instantly.

Compare this to SSR (Lesson 2.1), which is like a restaurant kitchen cooking each meal to order. SSR does fresh work for every request. SSG does the work once and serves the result forever — until you rebuild.

The trade-off is clear:

  • SSG = blazing fast, but content is frozen at build time
  • SSR = always fresh, but every request costs server computation

How SSG Works in Next.js (App Router)

In the App Router, SSG is the default behavior. If your page component doesn't do anything dynamic — no cookies, no headers, no searchParams, no uncached fetch — Next.js automatically generates it as a static page at build time.

Code Example 1: A Basic Static Page

// app/about/page.tsx
// This page is automatically static — no dynamic data, no user-specific content

export default function AboutPage() {
  return (
    <main>
      <h1>About Our Company</h1>
      <p>We build tools for developers.</p>
      <p>Founded in 2020. Based in San Francisco.</p>
    </main>
  );
}

// At build time, Next.js generates: .next/server/app/about.html
// Every visitor gets this exact same HTML file
// No server function runs per request

This page has no data fetching, no dynamic segments, nothing that changes between users. Next.js detects this and generates it as a static HTML file during next build. You can see this in the build output — static pages are marked with a symbol.

Code Example 2: Static Page with Data Fetching

// app/blog/page.tsx
// Fetches data at BUILD TIME, not at request time

async function getPosts() {
  // This fetch is cached by default in Next.js
  // It runs once at build time and the result is baked into the HTML
  const res = await fetch("https://api.example.com/posts", {
    cache: "force-cache", // This is the default — explicit here for clarity
  });
  return res.json();
}

export default async function BlogListPage() {
  const posts = await getPosts();

  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map((post: { id: string; title: string }) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </main>
  );
}

// Build output:
// ○ /blog — static (pre-rendered at build time with fetched data)
// The API is called ONCE during build. Not once per visitor.

Critical detail: The fetch call with cache: "force-cache" (or without any cache option, since it's the default) tells Next.js to cache the response. The page becomes static because its data is deterministic at build time.

The moment you add cache: "no-store" or access cookies() or headers(), the page becomes dynamic (SSR), not static.


generateStaticParams — Pre-Building Dynamic Routes

Static pages are straightforward when the URL is fixed (/about, /blog). But what about dynamic routes like /blog/[slug]? There could be hundreds of blog posts, each with a different slug. How does Next.js know which pages to pre-build?

That is exactly what generateStaticParams solves. It tells Next.js: "Here are all the possible values for this dynamic segment. Build a static page for each one."

Code Example 3: Static Dynamic Routes

// app/blog/[slug]/page.tsx

// Step 1: Tell Next.js which slugs to pre-build
export async function generateStaticParams() {
  const res = await fetch("https://api.example.com/posts");
  const posts = await res.json();

  // Return an array of objects — each object has the dynamic segment as a key
  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));

  // Example return value:
  // [
  //   { slug: "understanding-ssg" },
  //   { slug: "nextjs-vs-remix" },
  //   { slug: "react-server-components" },
  // ]
}

// Step 2: Build each page using the param
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

// Build output:
// ○ /blog/understanding-ssg
// ○ /blog/nextjs-vs-remix
// ○ /blog/react-server-components
// Each page is a separate static HTML file

What happens at build time:

  1. Next.js calls generateStaticParams() once
  2. It gets back a list of all slugs: ["understanding-ssg", "nextjs-vs-remix", ...]
  3. For each slug, it renders the page component and saves the HTML
  4. At runtime, visiting /blog/understanding-ssg serves a pre-built file — no computation

Code Example 4: Nested Dynamic Routes

// app/docs/[category]/[topic]/page.tsx
// Pre-build all combinations of category + topic

export async function generateStaticParams() {
  const categories = await fetch("https://api.example.com/categories").then(
    (r) => r.json()
  );

  // Return all category + topic combinations
  const params = [];

  for (const category of categories) {
    for (const topic of category.topics) {
      params.push({
        category: category.slug,
        topic: topic.slug,
      });
    }
  }

  return params;
  // Example:
  // [
  //   { category: "getting-started", topic: "installation" },
  //   { category: "getting-started", topic: "project-structure" },
  //   { category: "advanced", topic: "caching" },
  // ]
}

export default async function DocPage({
  params,
}: {
  params: Promise<{ category: string; topic: string }>;
}) {
  const { category, topic } = await params;
  const doc = await fetch(
    `https://api.example.com/docs/${category}/${topic}`
  ).then((r) => r.json());

  return (
    <article>
      <nav>Category: {category}</nav>
      <h1>{doc.title}</h1>
      <div>{doc.content}</div>
    </article>
  );
}

What If a User Visits an Ungenerated Path?

By default, Next.js will try to generate the page on-demand for slugs not returned by generateStaticParams. You can control this with dynamicParams:

// app/blog/[slug]/page.tsx

// If true (default): unknown slugs are rendered on-demand at request time
// If false: unknown slugs return 404
export const dynamicParams = false;

export async function generateStaticParams() {
  // Only these slugs will work — anything else is a 404
  return [{ slug: "post-1" }, { slug: "post-2" }];
}

Setting dynamicParams = false is useful when you know your content set is complete and you don't want unexpected paths to trigger server rendering.


When SSG Is the Right Choice

SSG is ideal when your content meets three criteria:

  1. The data is known at build time — you can fetch everything before deploy
  2. The content is the same for every user — no personalization
  3. The content changes infrequently — not real-time data

Perfect SSG Use Cases

Use CaseWhy SSG Works
Blog postsContent set at publish time, same for all readers
Documentation sitesVersioned content, changes only on new releases
Marketing/landing pagesCopy finalized by marketing team before deploy
Product catalog (small)Products change weekly, not per-second
Help center / FAQContent updated by support team, deployed on schedule
Portfolio sitesStatic showcase, rarely changes
Legal pages (terms, privacy)Change a few times per year

When SSG Breaks Down

ScenarioWhy SSG FailsBetter Strategy
User dashboardsDifferent data per userCSR or SSR
E-commerce pricesPrices change in real-timeISR or SSR
Social media feedsContent changes every secondSSR + CSR
Search resultsDepends on query paramsSSR
Live scores / stock pricesReal-time dataCSR with WebSockets
500K+ pagesBuild time becomes hoursISR (build on-demand)

Performance Benefits — Why SSG Is the Fastest Strategy

SSG pages are the fastest possible response a Next.js app can serve. Here is why:

1. No Server Computation Per Request

With SSR, every request triggers: parse request -> run component -> fetch data -> render HTML -> send response. With SSG, the server skips all of that and serves a file from disk (or memory).

2. CDN-Ready by Default

Static HTML files can be deployed to a CDN (Content Delivery Network) with edge nodes worldwide. A user in Tokyo gets the page from a Tokyo edge node in ~20ms, not from your origin server in Virginia at ~200ms.

SSR Request Flow:
User (Tokyo) -> CDN -> Origin Server (Virginia) -> Database -> Render -> Response
Latency: ~200-500ms

SSG Request Flow:
User (Tokyo) -> CDN Edge (Tokyo) -> Cached HTML file
Latency: ~20-50ms

3. Perfect Lighthouse Scores

SSG pages consistently achieve:

  • Time to First Byte (TTFB): Near-zero (file served from CDN)
  • Largest Contentful Paint (LCP): Minimal (HTML is complete, no client-side rendering)
  • Cumulative Layout Shift (CLS): Zero (content doesn't shift because it's all in the initial HTML)

4. Scales Infinitely with Zero Cost Increase

10 visitors or 10 million visitors — the cost per request is the same (essentially free with a CDN). SSR pages cost more as traffic grows because every request consumes server CPU.

Static Site Generation (SSG) visual 1


The SSG-to-ISR Bridge

A common interview follow-up: "What if your static content needs to update without a full redeploy?"

That is where Incremental Static Regeneration (ISR) comes in — covered in depth in Lesson 2.3. But here is the connection:

// app/blog/[slug]/page.tsx

// SSG: Page is built once and never changes until next deploy
// (default behavior — no revalidate option)

// ISR: Page is built once, then revalidated in the background
export const revalidate = 3600; // Rebuild this page at most every 1 hour

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await fetch(`https://api.example.com/posts/${slug}`).then((r) =>
    r.json()
  );

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

// Without revalidate: pure SSG — content frozen at build time
// With revalidate: ISR — content refreshes in the background
//   after the specified interval, next visitor triggers a rebuild

Think of it this way:

  • SSG = newspaper printed once per day (morning edition only)
  • ISR = newspaper that reprints a page when a correction comes in, without reprinting the entire paper

ISR is SSG's escape hatch for content that changes more often than your deploy cycle.


Build Output — How to Verify SSG

When you run next build, the output tells you exactly which pages are static:

Route (app)                    Size     First Load JS
 /                          5.2 kB        89 kB
 /about                     1.8 kB        85 kB
 /blog                      3.1 kB        87 kB
 /blog/[slug]               2.4 kB        86 kB
 ƒ /dashboard                 4.7 kB        88 kB
 /pricing                   2.1 kB        86 kB

  (Static)   pre-rendered as static content
  (SSG)      pre-rendered as static HTML (uses generateStaticParams)
ƒ  (Dynamic)  server-rendered on each request
  • = Fully static (no data fetching or only cached fetches)
  • = SSG with generateStaticParams (dynamic route, pre-built for known params)
  • ƒ = Dynamic / SSR (runs on every request)

If a page you expected to be static shows up as ƒ, something is making it dynamic — check for cookies(), headers(), searchParams, or cache: "no-store" in your fetches.


Common Mistakes

1. Assuming SSG Pages Update Automatically

SSG pages are frozen at build time. If your CMS content changes, the page does not change until you redeploy or add ISR. Developers often deploy an SSG blog, update a post in their CMS, and wonder why the live page still shows old content. You need either a redeploy trigger (webhook from CMS -> CI/CD rebuild) or ISR with revalidate.

2. Using cache: "no-store" and Expecting Static Output

Adding cache: "no-store" to any fetch call in your page opts the entire page out of static generation. Next.js silently converts it to a dynamic (SSR) page. This is the most common reason a page unexpectedly shows ƒ instead of in the build output.

// This page will NOT be static — it's dynamic because of no-store
async function getData() {
  const res = await fetch("https://api.example.com/data", {
    cache: "no-store", // This forces dynamic rendering
  });
  return res.json();
}

3. Forgetting generateStaticParams for Dynamic Routes

If you have a dynamic route ([slug]) and don't provide generateStaticParams, Next.js won't pre-build any pages for those routes. They will be generated on-demand at request time instead. This isn't necessarily wrong, but it means your first visitor to each page gets a slower SSR response rather than an instant static response.

4. Trying to SSG Everything (Including Personalized Content)

SSG serves the same HTML to every user. If your page shows "Welcome back, Sarah" or a personalized dashboard, SSG is fundamentally the wrong strategy. Use SSR for the initial shell or CSR for personalized sections within a statically generated page.


Interview Questions

Q1: What is Static Site Generation and how does it differ from Server-Side Rendering?

SSG generates HTML at build time — once, during next build. The resulting HTML files are served as-is to every visitor. SSR generates HTML at request time — the server runs the component, fetches data, and builds fresh HTML for each request. SSG is faster (pre-built files from CDN) but can only serve content known at build time. SSR is slower per-request but always serves fresh data.

Q2: How does generateStaticParams work in the App Router?

generateStaticParams is an async function you export from a dynamic route's page.tsx. It returns an array of objects, where each object contains the dynamic segment values. Next.js calls this function at build time and generates a static page for each returned set of params. For example, returning [{ slug: "post-1" }, { slug: "post-2" }] from app/blog/[slug]/page.tsx generates /blog/post-1 and /blog/post-2 as static HTML files.

Q3: A page you expected to be static shows as dynamic in the build output. What could cause this?

Several things opt a page out of static generation: using cache: "no-store" on any fetch call, accessing cookies() or headers() from next/headers, reading searchParams in a page component, or calling any function that requires per-request context. Any of these signals to Next.js that the page cannot be pre-built because it depends on request-time information.

Q4: When would you choose SSG over SSR? Give specific examples.

SSG for content that is the same for every user and changes infrequently: blog posts, documentation, marketing pages, FAQ pages, legal pages. SSR for content that varies per user or changes frequently: dashboards, search results, personalized feeds, real-time pricing. The key question is: "Can I build this page once and serve it to everyone?" If yes, SSG. If no, SSR.

Q5: What happens when a user visits a dynamic route path that was not included in generateStaticParams?

By default (dynamicParams = true), Next.js generates the page on-demand at request time — like SSR for the first visitor — then caches it as a static page for subsequent visitors. If you set dynamicParams = false, any path not returned by generateStaticParams returns a 404 page.


Quick Reference -- Cheat Sheet

+------------------------------------------------------------------+
|                STATIC SITE GENERATION (SSG)                      |
+------------------------------------------------------------------+
|                                                                  |
|  WHEN IT RUNS:    At build time (next build)                     |
|  OUTPUT:          Static HTML files in .next/server/app/         |
|  SERVED FROM:     CDN edge node (no origin server needed)        |
|  SPEED:           Fastest (~20-50ms TTFB from CDN)               |
|  BUILD SYMBOL:    ○ (static) or ● (SSG with generateStaticParams)|
|                                                                  |
+------------------------------------------------------------------+
|                                                                  |
|  DEFAULT IN APP ROUTER:  Yes (pages are static unless you opt    |
|                          into dynamic behavior)                  |
|                                                                  |
|  STATIC TRIGGERS:                                                |
|    - No cookies() / headers()                                    |
|    - No searchParams access                                      |
|    - All fetch() calls use cache: "force-cache" (default)        |
|    - generateStaticParams for dynamic routes                     |
|                                                                  |
|  DYNAMIC TRIGGERS (breaks SSG):                                  |
|    - cache: "no-store" on any fetch                              |
|    - cookies() or headers() usage                                |
|    - searchParams in page component                              |
|    - export const dynamic = "force-dynamic"                      |
|                                                                  |
+------------------------------------------------------------------+
|                                                                  |
|  IDEAL FOR:          NOT FOR:                                    |
|  - Blog / CMS        - User dashboards                          |
|  - Docs sites         - Real-time data                           |
|  - Marketing pages    - Personalized content                     |
|  - FAQ / Help center  - Search results                           |
|  - Portfolio          - High-frequency updates                   |
|                                                                  |
+------------------------------------------------------------------+
|                                                                  |
|  SSG + ISR BRIDGE:                                               |
|    export const revalidate = 3600; // Refresh every hour         |
|    (Turns pure SSG into ISR — Lesson 2.3)                        |
|                                                                  |
+------------------------------------------------------------------+

  generateStaticParams Flow:
  ┌─────────────────┐    ┌──────────────────┐    ┌───────────────┐
  │  next build     │───>│ generateStatic   │───>│ Render page   │
  │  starts         │    │ Params() called  │    │ for each param│
  └─────────────────┘    └──────────────────┘    └───────┬───────┘

                                                         v
                                                 ┌───────────────┐
                                                 │ Static HTML   │
                                                 │ files saved   │
                                                 │ to .next/     │
                                                 └───────────────┘

Previous: Lesson 2.1 — Server-Side Rendering (SSR) Next: Lesson 2.3 — Incremental Static Regeneration (ISR)


This is Lesson 2.2 of the Next.js Interview Prep Course -- 8 chapters, 33 lessons.

On this page