Rendering

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.

Build phase (once)

Seven phases from a code push or manual trigger to CDN deployment — all data-fetching and HTML generation completes here.

Developer / CI
Build (Node.js)
Data Source
DB / CMS / API
Storage (CDN)
  1. B1

    Run npm run build

    B1 Developer / CI 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.

  2. B2

    Enumerate all routes

    B2 Build (Node.js) Enumerate all routes

    getStaticPaths / generateStaticParams run to finalize every route to build — blog posts, products, tag pages are decided here.

  3. B3

    Data-fetch HTTP requests

    B3 Data Source 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.

  4. B4

    Responses from DB / CMS / API

    B4 DB / CMS / API 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.

  5. B5

    renderToString() for every route

    B5 Build (Node.js) 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).

  6. B6

    Write static files to dist/

    B6 Build (Node.js) Write static files to dist/

    Generated HTML + CSS + JS + images land in dist/ (or build/). This is the final deliverable.

  7. B7

    Upload to CDN

    B7 Storage (CDN) 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.

  8. Build complete — none of this re-runs at runtime
Runtime phase (per request)

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.

User
Browser
Network
CDN Edge
JS Engine
Compositor
GPU
Display
  1. R1

    URL entry / link click

    R1 User URL entry / link click

    The user opens a URL. The destination is a CDN, not an app server.

  2. R2

    Issue HTTP request

    R2 Browser Issue HTTP request

    The browser prepares and sends an HTTP request to the CDN.

  3. R3

    Route to nearest edge

    R3 Network Route to nearest edge

    Anycast routes the request to the geographically nearest edge. Japanese users hit Tokyo, Europeans hit Frankfurt, etc.

  4. R4

    Return cached HTML instantly

    R4 CDN Edge 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.

  5. R5

    Parse HTML → DOM / CSSOM

    R5 Browser 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.

  6. R6

    Style / Layout / Paint-record

    R6 Browser Style / Layout / Paint-record

    Run style calculation → layout → paint-record. With full content, the paint commands are substantial.

  7. R7

    Raster + layer composite

    R7 Compositor Raster + layer composite

    The compositor thread rasterizes tiles and builds composite commands from the layer tree.

  8. R8

    GPU builds the frame

    R8 GPU GPU builds the frame

    The GPU process runs the composite commands and builds the frame buffer.

  9. R9

    Content appears on screen — no JS needed

    R9 Display 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.

  10. FCP / LCP reached (no JS required)
  11. R10

    JS bundle transfer (optional)

    R10 Network 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.

  12. R11

    hydrateRoot() / selective island hydration

    R11 JS Engine hydrateRoot() / selective island hydration

    Only the parts that need it hydrate. Astro's islands architecture evaluates only islands marked with client:load etc., avoiding whole-page hydration and minimizing shipped JS.

  13. TTI reached — on a fully static site, FCP ≈ TTI is possible
  14. R12

    Fetch dynamic data via CSR (optional)

    R12 JS Engine 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:

01

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.

02

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.

03

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.
The decisive difference is when HTML is generated. SSR builds it per request on the server; SSG builds it once at build time and parks it on a CDN. Consequences: (a) TTFB: SSG > SSR, (b) freshness: SSR > SSG, (c) cost: SSG < SSR (no servers), (d) attack surface: SSG < SSR (no runtime code). Same final HTML, very different operational posture.
Q. How is ISR different from SSG? Or from SSR?
ISR extends SSG: mostly static, but selectively refreshable. Two modes: (a) time-based regeneration (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?
It takes planning. The login form itself is fine with SSG (same HTML for everyone). For post-login personalized data, pick one: (a) fetch from the client after hydration (CSR), (b) use an Edge Function for auth + dynamic content, (c) move the logged-in area to a separate domain running SSR or SPA. Pattern (a) is the most common — swap UI based on auth state after hydration.
Q. Build time is so long that CI chokes. What can I do?
Three options. (1) ISR / on-demand: only build the pages that truly need static generation; defer others until first request. (2) Use incremental-build-friendly frameworks / bundlers (Turbopack, Vite). (3) Separate image optimization from the main build — image processing often dominates build time, and parallelizing Sharp alone can dramatically improve it. Past the 10,000-page mark, pure SSG deserves reconsideration.
Q. I updated my SSG site but users still see the old content. Why?
Most likely a stale CDN cache. Common causes and fixes: (a) browser cache — include content hashes in HTML filenames or set a short 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?
They overlap, but aren't identical. JAMstack is an architectural pattern: JavaScript (on the client) + APIs + Markup (pre-built). SSG is the 'pre-built markup' part. JAMstack names the whole philosophy: static markup + CDN delivery + APIs for dynamic needs. A mix of SSG + ISR + Edge Functions also counts as JAMstack in the broader sense.

Design Signals

Where SSG shines — and where forcing it leads to trouble.

SSG fits here

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.

SSG fits here

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).

SSG doesn't fit

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.

SSG doesn't fit

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.