SSG (Static Site Generator)
A static site generator (SSG) pre-builds every HTML page at build time and serves it as static files. No server-side processing per request is needed, enabling fast delivery from a CDN.
From MDN Web Docs
A static site generator (SSG) is a software used to generate static websites. A static website is comprised of HTML, CSS, and JavaScript files. Most importantly static sites do not have server-side logic, so for any given URL, all users will receive the same content. Authors write content in any form accepted by the generator, such as Markdown, reStructuredText, HTML, (and sometimes even React, and so on), and the generator compiles them into a set of optimized static files that can be rendered by the browser.
Static sites are commonly used for blogs, documentation, and other content-driven websites, which don't have data that needs to be fetched or generated server-side. They are fast, secure, and easier to deploy, because they can be served from a CDN.
Metrics Primer: FCP / LCP / TTI
The swim-lane diagram below marks FCP / LCP / TTI milestones. Knowing what each metric actually measures upfront makes the diagram much easier to read.
- FCP First Contentful Paint — first visual content painted
- The moment text, images, SVG, or non-empty canvas is first painted on the browser viewport. A background-color-only screen doesn't count. A Core Web Vitals secondary metric — under 1.8 s is considered good.
- LCP Largest Contentful Paint — largest content painted
- When the largest content element (hero image, big heading, etc.) is painted. A proxy for 'meaningful first paint' and a primary Core Web Vitals metric. Under 2.5 s is good; over 4 s needs work.
- TTI Time to Interactive — when interactivity starts
- The moment the page is fully interactive — all event listeners attached and the main thread free of Long Tasks for 5 seconds. Dropped from the Core Web Vitals primary set (replaced by INP) in 2024, but still important as the 'feels usable' moment for real users.
Rendering Lifecycle
SSG cleanly separates its lifecycle into a build phase and a runtime phase — each with a different set of actors, so we draw them as two independent swim-lane diagrams. The decisive difference vs. SSR: no application server runs in the runtime phase.
Seven phases from a code push or manual trigger to CDN deployment — all data-fetching and HTML generation completes here.
-
B1 Run npm run build
A developer or CI (GitHub Actions / Vercel / Netlify) starts the build command. Triggered by a Git push or a manual run.
-
B2 Enumerate all routes
getStaticPaths/generateStaticParamsrun to finalize every route to build — blog posts, products, tag pages are decided here. -
B3 Data-fetch HTTP requests
Each route's data-fetch function (
getStaticProps/load/ Server Component fetch) issues HTTP requests during the build. Unlike SSR, this is a one-time affair. -
B4 Responses from DB / CMS / API
Data sources answer the build-time requests. Headless CMS (Contentful / microCMS), databases, and external APIs are hit here. Mind rate limits.
-
B5 renderToString() for every route
With the fetched data, generate HTML for each route — looping React / Vue / Svelte SSR functions on Node.js. Build time scales with page count (10k pages ≈ 10 minutes).
-
B6 Write static files to dist/
Generated HTML + CSS + JS + images land in dist/ (or build/). This is the final deliverable.
-
B7 Upload to CDN
The artifact is deployed to Vercel Edge / Netlify / Cloudflare Pages / S3 + CloudFront. From here, 'artifact' and 'runtime' are fully decoupled — no app server runs at runtime.
- Build complete — none of this re-runs at runtime
The 12 phases triggered by each user request, across eight lanes. No app server intervenes — the request flows CDN → Browser (Style/Layout) → Compositor (Raster) → GPU (frame buffer) → Display (vsync) to reach FCP / LCP. JS runs only afterwards, and only if needed.
-
R1 URL entry / link click
The user opens a URL. The destination is a CDN, not an app server.
-
R2 Issue HTTP request
The browser prepares and sends an HTTP request to the CDN.
-
R3 Route to nearest edge
Anycast routes the request to the geographically nearest edge. Japanese users hit Tokyo, Europeans hit Frankfurt, etc.
-
R4 Return cached HTML instantly
The edge returns the cached static HTML directly. TTFB lands in the 10–50 ms range. No application logic runs here, minimizing attack surface.
-
R5 Parse HTML → DOM / CSSOM
The browser parses the complete HTML into a DOM and, in parallel, CSS into a CSSOM. As with SSR, the DOM already contains real content.
-
R6 Style / Layout / Paint-record
Run style calculation → layout → paint-record. With full content, the paint commands are substantial.
-
R7 Raster + layer composite
The compositor thread rasterizes tiles and builds composite commands from the layer tree.
-
R8 GPU builds the frame
The GPU process runs the composite commands and builds the frame buffer.
-
R9 Content appears on screen — no JS needed
Pixels reach the screen in sync with vsync. Zero JavaScript has executed to get here. Reaching this milestone via only 'CDN → browser pipeline → screen' is SSG's biggest strength.
- FCP / LCP reached (no JS required)
-
R10 JS bundle transfer (optional)
Only if there's interactivity to wire up does the JS bundle travel from the CDN. A fully static site skips everything from here on, and FCP ≈ TTI.
-
R11 hydrateRoot() / selective island hydration
Only the parts that need it hydrate. Astro's islands architecture evaluates only islands marked with
client:loadetc., avoiding whole-page hydration and minimizing shipped JS. - TTI reached — on a fully static site, FCP ≈ TTI is possible
-
R12 Fetch dynamic data via CSR (optional)
Per-user or real-time pieces (logged-in name, cart count, notifications) aren't known at build time, so the client fetches them after hydration (CSR). Alternatively, Edge Functions handle them per request. Subsequent DOM changes flow through Browser → Compositor → GPU → Display just like a SPA.
ISR: Solving SSG's 'Can't Update' Problem
SSG's biggest weakness is that any update requires a full rebuild. ISR (Incremental Static Regeneration) relaxes this — pioneered by Next.js and now adopted by many frameworks. It lets you mark individual pages for regeneration on the next request.
Time-based (revalidate)
Mark a page with a TTL like '60 seconds'. The first request after expiry serves the stale HTML while regenerating in the background (stale-while-revalidate); subsequent requests get the new HTML. A popular way to stay 'mostly fresh' with minimal server work.
On-demand
When a CMS publishes an article or registers a product, a webhook triggers regeneration of just that page. Doesn't wait for user traffic, so update lag is near zero. A practical fit for CMS-driven SSG workflows.
How to Handle Dynamic Content
Trying to solve everything with pure SSG usually breaks down. Modern SSG is a hybrid: mostly static, with dynamic bits filled in by other mechanisms. Three canonical strategies:
CSR + Fetch (most common)
Ship static HTML as the 'shell' and fill dynamic bits from the browser via useEffect + Fetch. Good for small elements like cart badges, logged-in names, notification counts. Watch for (a) layout-shift skeletons and (b) fetch error handling.
Edge Functions for dynamic fragments
Handle specific endpoints or HTML fragments dynamically using Vercel Edge Functions / Cloudflare Workers / Netlify Edge Functions. Edge execution keeps latency low — ideal for auth checks, A/B tests, region-specific content. More predictable cost than full SSR.
Islands Architecture (Astro, etc.)
Keep most of the page fully static and run JavaScript only on interactive 'islands'. Each island hydrates independently, so a heavy island can't block others. Ship dramatically less JS, preserving SSG's speed advantage.
Frequently Asked Questions
Clearing up the relationships between SSG / ISR / JAMstack and the questions that always come up in practice.
Q. What's the real difference between SSR and SSG? Both return complete HTML.
Q. How is ISR different from SSG? Or from SSR?
revalidate: 60), (b) on-demand regeneration via webhook for specific pages. Unlike SSR, it does not regenerate on every request — only on expiry or trigger. It's the midpoint between SSG and SSR, and depends on the hosting platform (Vercel, Netlify, etc.).Q. Can I build login-gated pages with SSG?
Q. Build time is so long that CI chokes. What can I do?
Q. I updated my SSG site but users still see the old content. Why?
Cache-Control: max-age; (b) CDN cache — explicitly purge after rebuild (Vercel / Netlify auto-purge; self-hosted CloudFront needs an invalidation API call); (c) Service Worker — a cache-first strategy may retain stale HTML. Debug via Chrome DevTools' Network tab and check the 'from disk cache / memory cache / ServiceWorker' label.Q. Are JAMstack and SSG the same thing?
Design Signals
Where SSG shines — and where forcing it leads to trouble.
Most content is the same for every user
Docs, marketing sites, blogs, portfolios, OSS landing pages. Anything that can be built at build time should be — eliminating runtime server work is SSG's core strength.
Traffic is bursty or unpredictable
When traffic spikes, the pre-built static HTML is served straight from the CDN, so no server computation occurs. Per web.dev's "Rendering on the Web", static rendering offers "fast First Paint, First Contentful Paint and Time To Interactive" and fits CDN delivery naturally. SSR computes on each request inside the app server, so traffic spikes require separate scaling design (autoscaling / cold-start handling).
Content changes multiple times per day
News, SNS-style timelines, EC inventory / pricing — rebuilding all the time becomes impractical. ISR helps in mild cases, but past ~10 updates per day, SSR is simpler.
Per-user content is the core experience
Post-login dashboards, carts, account settings — 'different screen for every user' isn't what SSG is for. You can force it via CSR post-processing, but SPA or SSR is the cleaner architectural match.
Common Pitfalls
Page explosion breaks the build
At tens or hundreds of thousands of articles / products, pure SSG builds can take an hour or more. Fix: switch to ISR to build on access, or split 'hot pages → build now, long tail → build on first request' (Next.js fallback: true / blocking). Parallelizing image transforms alone can shave significant time.
Build-time API rate limits bite
Fetching data for every page during build can exhaust a CMS or external API's rate limit in a single build. Fix: (a) cache across builds and fetch only what changed, (b) use batch APIs if available, (c) negotiate a higher quota with the SaaS vendor, (d) spread load via ISR.
Trying to gate SSG pages behind auth
SSG HTML is broadly public. Putting auth-gated content into SSG can expose it to anyone. Fix: (a) do auth checks in client JS / Edge Functions and keep the HTML itself empty of sensitive content, (b) protect the data behind an authenticated API, (c) cleanly separate 'HTML that can be public' from 'data that cannot'.
Can't control update propagation lag
After a CMS update, old HTML and CDN caches linger, delaying propagation by minutes or more. Fix: (a) trigger builds from CMS publish events via webhooks, (b) use on-demand ISR regeneration, (c) call the CDN purge API (Cloudflare / CloudFront). The key operational practice is turning 'when updates land' into an explicit SLA.