ibit
Build log

Next.js 16 with no server

WRWhite Rabbit·June 29, 2026·4 min

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.