Static Site Generation
Render every page once at build time and serve the files from an edge. The cheapest thing to serve, and the hardest thing to keep current.
The intent, the obvious build, and why it breaks
Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.
What does moving rendering to build time actually buy, and what does it make expensive?
Someone wants to read a page — documentation, an article, a product listing — that is the same for them as it is for everybody else.
If the output is the same for every visitor, render it once during the build and upload the HTML. There is no server to run, no query at request time, and the file can sit on an edge cache next to the user.
The data is as fresh as the last build. A price, a stock count or a published status that changed five minutes ago is served confidently and wrongly, which is worse than being slow (The Client Cache Model).
- The data is as fresh as the last build. A price, a stock count or a published status that changed five minutes ago is served confidently and wrongly, which is worse than being slow (The Client Cache Model).
- Build time scales with page count. A site of a few hundred pages builds in a moment; a catalogue of hundreds of thousands renders every one of them on every deploy, and the build becomes the deployment bottleneck (Deploying a Frontend).
- The page cannot be personal. Anything that depends on who is asking — a name, a permission, a cart, a saved filter — either is not there or has to be filled in on the client after paint.
- Every page must be enumerable at build time. A route with an unbounded parameter space cannot be pre-rendered exhaustively, so something has to decide which subset gets built and what happens to the rest.
- It says nothing at all about JavaScript. A statically generated page can still ship a full application bundle and still hydrate the whole tree, so the early paint can be followed by exactly the interactivity gap server rendering has (Hydration).
- Content changes now require a deploy. An editorial team that could previously publish is now coupled to your build pipeline, which is an organisational cost more often than a technical one.
What is actually happening
In the browser, not in the framework.
- At build time a process walks a list of routes, fetches whatever each one needs, renders the component tree to HTML exactly as a server renderer would, and writes a file per route (Server-Side Rendering describes the same render, just at a different moment).
- Those files are uploaded to object storage and served through a CDN. A request is answered from an edge cache with no origin work, no database connection and no query in the path (CDN Delivery).
- Because the response is identical for everyone, every layer between your origin and the user is allowed to cache it, which is why this is the cheapest strategy to serve under load (Browser HTTP Caching).
- The client side is unchanged: the same bundle downloads and the same hydration pass runs. Static generation moved where the HTML came from and nothing else (Hydration).
- Freshness is therefore a build-scheduling problem rather than a request-handling one. The levers are how often the build runs, whether it can rebuild a subset, and what the client does after paint to top up what the build could not know (Stale-While-Revalidate).
- Many frameworks offer a middle position — regenerate a page on demand, or after a stale interval, and cache the result — which is a per-request render with a cache in front of it rather than a genuinely static file. The distinction matters because the failure modes are the server rendering ones again (Server-Side Rendering).
What this makes the browser do
And which of it is avoidable.
- Parse a complete document, exactly as with server rendering, with the same tree construction, style and layout costs (Tree Construction).
- Nothing extra at request time: no serialized state has to arrive from a per-request render, though the build usually embeds the same blob so hydration has something to reconcile against.
- The full application bundle, if the page ships one — which is the row nobody looks at, because early paint makes the page feel finished (The Real Cost of JavaScript).
- Avoidable: hydrating a page of text that has two interactive controls in it, which is precisely the case Islands and Partial Hydration exists for.
- Avoidable: a client-side fetch on mount to correct data the build got wrong, which reintroduces a round trip the strategy was supposed to remove (The Life of a Fetch).
The request path when there is no server
The milestones look better than every other strategy at the top and identical to server rendering at the bottom, and that split is the whole lesson. Static generation removes the origin from the request path entirely, which is why the document arrives first and content paints earliest. It does nothing whatsoever about the bundle.
Read the distance between Content visible and Interactive here and compare it to Server-Side Rendering. It is the same distance. Moving the render to build time made the page appear sooner and made the gap between appearing and working slightly longer, because the paint moved and the hydration did not.
- Data ready — Before the request existed — during a build that may have run hours ago. That is the entire advantage and the entire limitation in one row.
- HTML arrives — The earliest first byte in the module: a file from an edge cache, with no origin work and no query in the path.
- Content visible — The earliest content of the five strategies. Nothing had to be computed and nothing had to be fetched.
- JS downloaded — Unchanged. Static HTML says nothing about how much JavaScript the page then loads, and this row is where that gets forgotten.
- Hydration — The same full-tree pass server rendering pays. Content arriving early does not make interactivity arrive early.
- Interactive — Content and interactivity are separate problems solved by separate mechanisms, and this strategy only solves the first.
Units are ordering, not duration. The comparison that matters is between rows within a timeline and between the same row across timelines.
Where the work went
It helps to look at the pipeline as a whole, because the strategy did not delete work — it moved it into a process that runs on a schedule you control rather than on a schedule your visitors control. Everything that was expensive per request is now expensive per build, and everything that was cheap per build is now the thing that limits how often you can publish.
The step that fails most interestingly is enumeration. Deciding which routes exist is trivial for a documentation site and genuinely hard for a catalogue, and it is where a static build stops being a static build and starts being a hybrid.
- 1Enumerate routes
Produces the list of pages to render, usually from a filesystem tree or a data source query.
fails by Missing pages that exist logically but were never in the list, so a valid URL returns a 404 that no test covers.
- 2Fetch data
Reads whatever each page needs, once, with build-machine credentials.
fails by Rate limiting or timing out against the data source when the page count grows, turning a successful build into an intermittent one.
- 3Render to HTML
Runs the component tree to a string per route — the same render a server would do, at a different moment.
fails by Throwing on a single bad record and failing the whole build, or silently emitting a broken page if errors are swallowed per route.
- 4Emit assets
Writes the HTML files plus content-hashed CSS and JavaScript.
fails by Hashing the assets but not invalidating the HTML, so cached documents reference files that no longer exist (Content-Hashed Assets).
- 5Publish to the edge
Uploads and invalidates, so the CDN serves the new build.
fails by Partial publication: new HTML live while some assets are still propagating, producing a window of broken pages.
- 6Serve a request
Returns a cached file with no origin involvement.
fails by A cache miss quietly falling through to an origin nobody sized for it.
- 7Hydrate in the browser
Downloads the bundle and attaches behaviour to the pre-rendered markup.
fails by Build-time markup disagreeing with what the client renders now — a timestamp, a locale, a value that has changed since (Hydration Mismatch).
Only the last two steps happen while a user is waiting. That asymmetry is why this strategy is cheap to serve and awkward to keep current.
Choosing how the content stays current
Freshness is the only genuinely hard question here, and it has more than two answers. Treating it as "static or dynamic" forces an all-or-nothing choice on a page whose parts usually have very different requirements: an article body that changes when someone edits it, a byline that never changes, and a comment count that changes constantly.
The useful move is to pick per value rather than per page. Most pages that people describe as impossible to generate statically turn out to be a static document with two volatile numbers in it.
This page is generated at build time. How does it stay accurate?
when Page count is small enough that a full build is fast, and content changes are infrequent and human-triggered.
cost Build duration scales with the catalogue, and every change — however small — waits for the whole pipeline. Eventually nobody runs it.
when The generator can map a content change to the set of pages it affects, and that mapping is trustworthy.
cost The mapping is the hard part: a change to a shared component or a navigation entry affects every page, and getting the dependency graph wrong leaves pages stale with no signal.
when The long tail is large, traffic is concentrated, and a first-request penalty after expiry is acceptable.
cost This is a per-request render with a cache in front. You are back to operating a server, and the personalization and caching caveats of Server-Side Rendering apply again.
when Most of the page is stable and one or two values are not — a count, a price, a live status.
cost A round trip and a bundle after paint, a pending state to design, space to reserve, and an announcement for anyone who cannot see the value change (Live Regions and Announcement).
when The first view is personal, permission-dependent, or changes faster than any build interval.
cost Per-request server work and everything that comes with it — which for this route is the correct price rather than an overrun (Choosing a Rendering Strategy).
How to build it
Most important first.
- Ask what "fresh enough" means for this content, and get a person to answer it out loud. A documentation page can be hours stale; a price cannot be minutes stale; a stock count probably cannot be static at all (Choosing a Rendering Strategy).
- Split the page by freshness rather than by strategy. Generate the stable majority, and fetch the few volatile values after paint with a visible pending state, so the page is right rather than uniformly stale (Loading, Error, Empty — The States You Did Not Render).
- Bound the build. Pre-render the pages that get traffic and let the long tail render on demand and be cached, so build time tracks a decision rather than a catalogue size.
- Reserve space for anything filled in after paint, or the correction will shove content a person is already reading (Visual Stability).
- Content-hash the assets and give the HTML a short freshness window, so a rebuild reaches users promptly while the bundles stay cacheable forever (Content-Hashed Assets).
- Keep the interactivity question separate and answer it separately. Static generation solved when content arrives; whether the page ships an application bundle is still entirely up to you (Code Splitting).
Keyboard, focus, semantics, announcement
A required field on every lesson in this domain, not a section added when there is room.
- Content is in the document at first paint, so it is in the accessibility tree before any script runs — the same benefit server rendering gives, without the per-request cost (The Accessibility Tree).
- Anything filled in after paint needs to be announced rather than merely appear. A price that silently changes from a build-time value to a live one is a change a screen-reader user has no way to notice (Live Regions and Announcement).
- Content that moves after paint is a problem for everyone and a larger one for people using screen magnification, who may be looking at a small region of the page when it reflows (Visual Stability).
- The interactivity gap is unchanged. Early paint makes a statically generated page look ready sooner, which means the window in which a keyboard user can press Enter on an inert control is longer, not shorter (Hydration).
- Static output is only as accessible as the components that produced it. Generating a page of
divs at build time produces a page ofdivs very efficiently (Semantics Are Behaviour).
What can go wrong
- Stale content served with total confidence. There is no loading state, no error and no signal — the page looks perfect and is wrong (Stale-While-Revalidate).
- A build that fails after a content change, leaving the previous version live. That is the right failure mode, but only if somebody is told (Release Health).
- A build that takes long enough that nobody runs it, so publishing becomes batched and the staleness window quietly grows.
- A stale HTML document referencing hashed assets from a build that has since been replaced, producing a page that loads and then fails to hydrate (Long-Lived Clients and Version Skew).
- The mitigation failing: topping up volatile data on the client, and shipping a bundle to do it that costs more than server rendering the page would have.
- Personalising after paint without reserving space, so every visitor watches the page rearrange itself once (Visual Stability).
- A deploy replaces the HTML while a user is holding an older document, so their next navigation requests an asset the new build no longer has (Long-Lived Clients and Version Skew).
- A client-side top-up for a volatile value resolves after the user has already read and acted on the build-time value.
- Two builds triggered by two content changes finishing out of order, publishing the earlier content last, unless the pipeline serialises them (Deploying a Frontend).
- The response is identical for everyone, which removes the entire class of caching bugs that per-request personalization creates. This is the security advantage of the strategy and it is a real one (Browser HTTP Caching).
- It also means nothing in the output may be permission-dependent. A page generated for an administrator and served from an edge is served to everybody who requests that URL (Authorization-Aware UI).
- The build machine holds credentials for whatever it fetched. Values that were only needed to render must not end up in the emitted HTML, and the review is the same one server rendering needs (Storage Security and Durability).
- Static hosting removes an origin to attack and does not remove the API the client still calls. Every authorization check still lives there (What the Frontend Is Responsible For in Auth).
- Content injected into the build from an external source — a CMS, a markdown corpus, a partner feed — is untrusted input that gets baked into your document and served from your origin (Sanitization and Trusted HTML).
- "Static means fast." It means cheap to serve and early to paint. If the page ships a large bundle it can still be unresponsive for as long as any other strategy (Hydration).
- "Static means no JavaScript." Static describes where the HTML came from. The two questions are independent, and conflating them is the most common error in this module.
- "We can just rebuild on every change." Until the build takes longer than the interval between changes, at which point the pipeline is permanently behind.
- "Incremental regeneration makes it static and fresh." It makes it a per-request render with a cache in front, with the server costs and the personalization caveats that implies (Server-Side Rendering).
- "Static sites cannot be dynamic." They can be, on the client, after paint — the question is whether the dynamic part is worth the bundle that fetches it.
Measuring it, and what changes in the field
- Time to the first byte should be edge-cache time, and if it is not, requests are reaching the origin and the strategy is not doing what you think (Debugging the Network).
- Cache hit ratio at the CDN is the metric that tells you whether the files are actually being served from the edge (CDN Delivery).
- Build duration and page count, tracked over time. The failure here is gradual: nobody notices the build crossing from minutes into an hour until a hotfix needs to ship.
- The age of the content in the response, exposed deliberately, so "is this stale" is answerable without guessing.
- The same view-source test as server rendering: what arrived is what a non-executing client gets, and it is also what a search engine indexes (A Mental Model of the Devtools).
- Under a traffic spike this is the strategy that does not care. Serving a cached file to a hundred thousand people costs what serving it to one costs (CDN Delivery).
- When the origin is down, statically generated pages keep serving, because nothing at request time depends on it — which makes this a resilience decision as much as a performance one.
- On a slow network the early document helps exactly as much as server rendering does, and the absence of origin work helps more.
- As the catalogue grows, the build is the thing that degrades, and it degrades in the deployment pipeline where users cannot see it until a release is urgent.
- On a low-end device the paint is early and the hydration cost is unchanged, so the felt experience is "instant page, unresponsive page" unless the bundle was addressed separately (The Real Cost of JavaScript).
- The cheapest, most resilient delivery in the module, bought with content that is exactly as current as the last successful build.
- No server runtime to operate, bought with a build that must enumerate and render every page and that grows with the catalogue.
- A uniform response for everyone, which is both why it caches perfectly and why it cannot say anything personal without a second, client-side step.
Where this applies
Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.
- GENERALPre-rendering at build time and serving files from a cache is a deployment shape rather than a framework feature, and the freshness-versus-build-cost trade holds whether the generator is a framework, a documentation tool or a script that writes HTML.
- FRAMEWORK-SPECIFICOn-demand and incremental regeneration are framework and host features with materially different semantics: some regenerate in the background and serve stale meanwhile, some block the first request after expiry, and some require a specific hosting platform — so a page described as "static" in one stack may be a cached per-request render in another.
Where the depth lives
This domain teaches the browser-side mechanism and hands the rest off.
- — Testing & Reliability Engineering: a build that renders every page is also the largest integration test the project has, and treating a per-page render failure as a build failure rather than a warning is a reliability decision.