This blog has no backend. There is no Node process answering requests, no container to keep warm, no function spinning up on a cold start. When you loaded this page, a file server handed you an HTML file that was finished days ago. The whole site is a folder.
That's not a downgrade I settled for. It's the thing I was after. The interesting
part is that I didn't have to give up the App Router, Server Components, or any
of the Next.js authoring model to get it. The single line that makes it happen
lives in next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
};
export default nextConfig;
After next build, Next.js writes an out/ folder of HTML/CSS/JS and nothing
else. The docs put it
plainly: a static export can be "hosted on any web server that can serve
HTML/CSS/JS static assets." No Node server required.
The mental shift: there is no "request time"
The thing to internalize is that every moment of computation moves to one
place — the build. When you write an async Server Component that fetches data,
that fetch doesn't run when a visitor arrives. It runs once, on my machine (or in
CI), during next build. The component renders to HTML, the HTML gets written to
disk, and that's the artifact every future visitor receives.
// app/page.tsx — this runs at BUILD time, not on request
export default async function Page() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return <PostList posts={posts} />;
}
The docs confirm this directly: Server Components "run during the build process, similar to traditional static-site generation," producing "static HTML for the initial page load and a static payload for client-side navigation." So the model isn't "server-rendered, just cached harder." It's that the server phase happened before deploy and then the server went away. What ships is the frozen output.
The browser still gets a real React app. It hydrates, next/link does
client-side route transitions, and from there it behaves like a SPA — page
swaps happen in the browser against those pre-built static payloads, with no
round trip to an origin that runs your code.
What going server-less actually buys you
Cacheability, for free and total. Every route is a static file, so it's
trivially cacheable end to end — CDN edge, browser, a proxy in between, all of it.
There's no Cache-Control puzzle about which responses are safe to store,
because they're all just files. The fastest response is the one your server never
computes, and here the server computes nothing.
Hosting collapses to "put files somewhere." Any static host works — a CDN, an S3 bucket, nginx, GitHub Pages. There's no runtime to provision, no region to pick, no autoscaling policy. Scaling a static site is the host's already-solved problem of serving files, not yours.
And the attack surface mostly evaporates. There is no request-handling code in production to exploit — no SSR path taking untrusted input, no server-side secrets to leak because there's no server to hold them. (The flip side: anything you ship is public, so a secret in the bundle is a secret in the browser.) What isn't running can't be attacked, and can't fall over under load.
What you give up
Honestly: the per-request render, and everything that depends on it. No reading cookies or headers on the server, no redirects/rewrites computed at the edge, no Server Actions, no on-the-fly image optimization with the default loader, no Incremental Static Regeneration. If a page's content depends on who is asking or when, that branch has to move into the browser as a Client Component calling an external API, or it can't exist here at all.
That's the trade, and it's a real one — I'll map the exact boundary in a separate note. But for a blog, the per-request render was never doing anything. The content is the same for everyone. Paying for a running server to re-derive identical HTML on every visit was the strange part; shipping it once as files is the obvious one.
The reframe that stuck with me: "static" isn't a stripped-down mode of Next.js. It's Next.js run to completion. The server did its job at build time and then I threw it away, because nothing left to do needs it.