Suggest an editImprove this articleRefine the answer for “How static site generation (SSG) works in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Static Site Generation (SSG)** in Next.js pre-renders HTML pages at build time using `next build`, then serves them from a CDN with no server computation per request. ```tsx const data = await fetch('https://api.example.com/post', { cache: 'force-cache' // runs once at build, never again per request }).then(r => r.json()); ``` **Key point:** rendering moves from request time to build time. TTFB drops to ~10ms from CDN vs 100-500ms for SSR. For data that updates after deploy, use ISR with `export const revalidate = 60`.Shown above the full answer for quick recall.Answer (EN)Image**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()` or `searchParams` inside a component will silently opt that page out of SSG during build ### Quick Example ```tsx // 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 client ``` This 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: ```tsx // 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: ```tsx // 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 HTML ``` ISR 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** ```tsx // 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** ```tsx // app/shop/[id]/page.tsx with no generateStaticParams // Result: SSR fallback, zero pre-built HTML ``` Every dynamic route without `generateStaticParams` automatically becomes SSR. Add it or accept the tradeoff consciously. **Using `cookies()` in a component that looks static** ```tsx // 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)** ```tsx // 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: `generateStaticParams` pre-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 ```tsx // 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 request ``` `cache: 'force-cache'` locks the response to build time. This is the simplest SSG setup in App Router. ### Dynamic Routes with `generateStaticParams` ```tsx // 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` ```tsx // 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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.