How static site generation (SSG) works in Next.js
Static Site Generation (SSG) in Next.js pre-renders pages into HTML files during next build, then serves them from a CDN with no server code running per request.
Theory
TL;DR
- SSG = baking all the cookies before the shop opens; visitors grab one instantly, no oven time per customer
- Main difference from SSR: rendering happens at build time, not on every request
- TTFB: ~10ms from CDN vs 100-500ms for SSR
- Use SSG when content changes less than once a day and speed or SEO matters
cookies()orsearchParamsinside a component will silently opt that page out of SSG during build
Quick Example
// app/page.tsx - runs once at build time, outputs static HTML
export default async function HomePage() {
const data = await fetch('https://api.example.com/posts/1', {
cache: 'force-cache' // locks the request to build time
}).then(res => res.json());
return (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
);
}
// Output: .next/server/app/index.html with data baked in
// On request: CDN returns HTML in ~10ms, JS hydrates the clientThis component runs once during next build. The API response is embedded in the HTML. When users visit, the server is not involved at all.
Build Time vs Request Time
SSR runs Node.js on every request. SSG runs it exactly once. That single difference is what lets SSG serve pages in under 100ms from a CDN edge, while SSR still needs 100-500ms for the server roundtrip. The tradeoff: SSG data is frozen until the next build or ISR revalidation.
When to Use SSG
- Documentation, blogs, marketing pages: stable content, SEO matters, traffic can spike unexpectedly
- Product catalogs with weekly updates: SSG with ISR (
revalidate: 60) covers this well - High traffic on a tight server budget: SSG removes runtime compute costs entirely
- User dashboards or personalized pages: skip SSG, use SSR or CSR instead
- Real-time data (stock prices, live chat): SSG is not an option here
Comparison Table
| SSG | SSR | CSR | |
|---|---|---|---|
| Render time | next build | Every request | Browser |
| TTFB | ~10ms (CDN) | 100-500ms | 2-5s (JS bundle) |
| Server cost | None after deploy | Scales with traffic | Minimal (static host) |
| SEO | Full pre-rendered HTML | Full pre-rendered HTML | Weak (needs hydration) |
| Data freshness | Frozen until rebuild or ISR | Always fresh | On-demand |
| Next.js API | Default App Router / getStaticProps | getServerSideProps | useEffect |
| Best for | Docs, blogs (Vercel.com) | Dashboards (Stripe.com) | SPAs (Gmail) |
How Next.js Builds Static Pages
During next build, Next.js scans every page, runs React Server Components in Node.js, fetches data via fetch with cache: 'force-cache', serializes the React tree to HTML using renderToPipeableStream, and writes .html files to .next/server/app. After that the server does nothing on requests. The CDN handles everything.
For dynamic routes, generateStaticParams tells the build which paths to pre-render:
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json());
return posts.map((post: { slug: string }) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
cache: 'force-cache'
}).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// Build generates: /blog/my-first-post/index.html, /blog/next-tips/index.html, ...Without generateStaticParams on a dynamic route, Next.js falls back to SSR. That is a common performance regression teams catch late.
ISR: Static Pages That Can Update
ISR (Incremental Static Regeneration) lets you revalidate a page in the background without a full rebuild. Add export const revalidate = 60 to revalidate every 60 seconds:
// app/blog/[slug]/page.tsx
export const revalidate = 60; // background revalidation every 60s
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: 60 }
}).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// First request after 60s: serves stale HTML, regenerates in background
// Second request: gets fresh HTMLISR is what makes "mostly static" content practical. Stripe's API Reference docs use this pattern to stay current without rebuilding everything.
Common Mistakes
Fetch without cache: 'force-cache' in App Router
// Wrong: may default to no-store in some Next.js 14+ configurations
const data = await fetch('/api/data');
// Correct
const data = await fetch('/api/data', { cache: 'force-cache' });
// Or at the page level: export const dynamic = 'force-static';Without force-cache, the page becomes a dynamic render. Build logs show ⚠ Dynamic and the CDN has nothing to cache.
Missing generateStaticParams on dynamic routes
// app/shop/[id]/page.tsx with no generateStaticParams
// Result: SSR fallback, zero pre-built HTMLEvery dynamic route without generateStaticParams automatically becomes SSR. Add it or accept the tradeoff consciously.
Using cookies() in a component that looks static
// This component looks static but it is not
export default async function Dashboard({ params }: { params: { id: string } }) {
const isLoggedIn = !!cookies().get('session')?.value; // forces dynamic rendering
return <div>User {params.id}</div>;
}cookies() marks the page dynamic = 'force-dynamic' during build analysis. The page becomes SSR regardless of generateStaticParams. Check next build logs for "Dynamic Function" warnings. I have seen this catch teams off guard when they already had generateStaticParams set up and assumed everything was static.
revalidate: 0 in ISR
Setting revalidate: 0 disables static generation entirely. Use revalidate: false for pure static or any positive number for ISR.
Mutating data in getStaticProps (Pages Router)
// Wrong
export async function getStaticProps() {
const data = await fetch('/api').then(r => r.json());
data.count++; // mutation is ignored or crashes the build
return { props: { data } };
}
// Correct
export async function getStaticProps() {
const data = await fetch('/api').then(r => r.json());
return { props: { data: { ...data, count: data.count + 1 } } };
}getStaticProps should be pure. Transform immutably.
Real-World Usage
- Vercel.com docs: SSG with ISR, 10M+ monthly visitors, zero server compute on page requests
- TailwindUI.com:
generateStaticParamspre-builds 500+ component preview pages - Stripe.com docs: SSG + ISR for API reference updates without full rebuilds
- TinaCMS + Next.js: headless CMS triggers rebuilds on content change, a classic Jamstack setup
Follow-Up Questions
Q: How does Next.js decide whether to render a page statically or dynamically?
A: During next build, it scans each page for dynamic functions: cookies(), headers(), searchParams. Finding any of these marks the page force-dynamic. Build output shows ○ (Static) vs λ (Dynamic) for each route.
Q: What is the difference between export const revalidate = 60 and next: { revalidate: 60 } in fetch?
A: Page-level revalidate sets a default for the whole route. Fetch-level next.revalidate overrides per request and takes precedence, so one page can have one slow-revalidating fetch and one fast one.
Q: Why does cookies() prevent SSG?
A: It signals that the page depends on per-request context. Next.js cannot pre-render something that differs per user, so it marks the page dynamic = 'force-dynamic' and skips static generation entirely.
Q: ISR vs full rebuild: when does each make sense?
A: ISR regenerates single pages in the background with no downtime. A full rebuild touches everything at once and is faster for small sites. For 50 pages, a full rebuild is fine. For 10,000 product pages, ISR is the only practical path.
Q: What happens to paths not listed in generateStaticParams?
A: By default (dynamicParams = true), unknown paths fall back to SSR. Set export const dynamicParams = false to return 404 for anything not pre-built at build time.
Examples
Basic SSG Page in App Router
// app/docs/page.tsx
export default async function DocsPage() {
const docs = await fetch('https://api.itlead.org/docs', {
cache: 'force-cache'
}).then(res => res.json());
return (
<ul>
{docs.map((doc: { slug: string; title: string }) => (
<li key={doc.slug}>{doc.title}</li>
))}
</ul>
);
}
// Build output: docs/index.html with all titles baked in
// CDN serves this file directly, no Node.js involved on requestcache: 'force-cache' locks the response to build time. This is the simplest SSG setup in App Router.
Dynamic Routes with generateStaticParams
// app/docs/[slug]/page.tsx
export async function generateStaticParams() {
const docs = await fetch('https://api.itlead.org/docs').then(r => r.json());
return docs.map((doc: { slug: string }) => ({ slug: doc.slug }));
}
export default async function DocPage({ params }: { params: { slug: string } }) {
const doc = await fetch(`https://api.itlead.org/docs/${params.slug}`, {
cache: 'force-cache'
}).then(res => res.json());
return (
<article>
<h1>{doc.title}</h1>
<div>{doc.content}</div>
</article>
);
}generateStaticParams runs once at build time, returns all slugs, and Next.js generates one HTML file per slug. At runtime it is pure CDN, no server.
Pages Router with getStaticProps and getStaticPaths
// pages/docs/[slug].tsx
export async function getStaticPaths() {
const docs = await fetch('https://api.itlead.org/docs').then(r => r.json());
return {
paths: docs.map((doc: { slug: string }) => ({ params: { slug: doc.slug } })),
fallback: false // 404 for any unknown slug
};
}
export async function getStaticProps({ params }: { params: { slug: string } }) {
const doc = await fetch(`https://api.itlead.org/docs/${params.slug}`).then(r => r.json());
return { props: { doc } };
}
export default function DocPage({ doc }: { doc: { title: string; content: string } }) {
return (
<article>
<h1>{doc.title}</h1>
<p>{doc.content}</p>
</article>
);
}fallback: false means unlisted slugs return 404 immediately. Switch to fallback: 'blocking' if you want SSR fallback for new content added after the build.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.