Architecture · · Updated · 8 min read

Astro over Next.js — the frontend decision that mostly never gets made

When a frontend is chosen, the decision remarkably often falls only between the “big” ones: Next, Nuxt, maybe SvelteKit. The question that comes before — does this site even need that? — gets skipped.

I have run both variants on my own site: Astro with Payload CMS, and Astro fully static with the content right in the repository. The reason was not framework frustration but the question of how much technology content maintenance really needs — tested on a real project rather than on paper. It could just as well have been a Next-to-Astro move; the lesson is the same.

The decision that mostly never gets made

The real problem is not “Astro or Next” but that the choice is reduced to “which of the big frameworks”. Astro rarely even makes the list — although for a website, a landing page or a marketing site it is completely sufficient and, on maintenance, performance and bloat, often the better choice.

Where Astro concretely wins: islands and JS load

This is the core. Most marketing sites show content, a slider, a bit of animation. A SPA-first framework ships a JavaScript runtime for that — whether you use it or not. Astro’s islands model ships HTML by default and JavaScript only for the few interactive spots. Less JavaScript shipped is the single biggest lever for page speed.

That is exactly why good page speed scores come more easily with Astro — not because Astro has a magic optimizer, but because the default is “no JavaScript until asked”. You reach the scores by not shipping the problem in the first place. SSR works just as it does elsewhere — you are simply lighter on your feet.

Where Next.js stays the right choice

Next has its place, and it is a real one: e-commerce, large applications with lots of dynamic content, deeply interactive product surfaces, a shared stack with the backend. There the weight buys something.

But even there it is now worth checking whether Astro carries it as well. This is not an anti-Next text. The point is not to reach for Next reflexively for a brochure site, just because it is the familiar tool.

The apparent contradiction: Astro and Next in the same project

The punchline: even if you choose Astro, there is often still a Next.js app in the project — at the latest once a CMS comes in. A typical setup, as I have run it myself: a pnpm monorepo of two parts — the public frontend in Astro with Tailwind, and a Payload CMS that runs as a thin Next app because Payload v3 is Next.js-native.

Broken down to the essentials it looks like this:

example-website/                     pnpm monorepo
├── apps/
│   ├── web/                          Astro 6 — the public site
│   │   └── src/
│   │       ├── pages/                file routing (/, /de/...)
│   │       └── lib/payload.ts        the ONLY place that calls the CMS
│   └── cms/                          Payload on Next.js — CMS host only
│       └── src/
│           ├── collections/          Posts, Pages, ...
│           └── payload-types.ts      auto-generated types
├── .env                              one source (per-app symlinks)
└── pnpm-workspace.yaml               packages: apps/*

The coupling between the two is deliberately thin — and exactly for that reason easy to rebuild. The web reads the CMS types via a tsconfig alias, without ever copying the file:

// apps/web/tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@cms-types": ["../cms/src/payload-types.ts"]
    }
  }
}

And it fetches the content through exactly one typed place, by API key against the Payload REST API:

// apps/web/src/lib/payload.ts (simplified)
import type { Post } from "@cms-types";

const PAYLOAD_URL = process.env.PAYLOAD_URL ?? "http://localhost:3000";
const API_KEY = process.env.PAYLOAD_API_KEY ?? "";

async function payloadFetch<T>(path: string): Promise<T> {
  const headers = new Headers({ Accept: "application/json" });
  if (API_KEY) headers.set("Authorization", `users API-Key ${API_KEY}`);
  const res = await fetch(`${PAYLOAD_URL}/api${path}`, { headers });
  if (!res.ok) throw new Error(`Payload ${res.status}`);
  return (await res.json()) as T;
}

export const getPosts = (locale: "de" | "en") => payloadFetch<{ docs: Post[] }>(`/posts?locale=${locale}`);

There is no more connection than that: one type alias and one fetch module. Next does what it is genuinely good at — the admin and app surface of the CMS, including live preview. Astro does what it is good at — fast, lean content delivery, bilingual DE/EN, deployed via Dokploy for example. The split here is the architecture, not a compromise.

And if only one person maintains the content, it gets leaner still: this site now runs without any CMS — Markdown in the repository, built statically, served via Cloudflare. A CMS pays off once several people maintain content without Git; before that it is mostly extra operations.

Conclusion

The frontend decision is not “which of the big frameworks” but “what does this site actually do?”. For marketing and content sites the answer is mostly Astro: less to maintain, less JavaScript, faster out of the box. You keep the heavy frameworks for the places where their weight buys something.

If a frontend decision is on your table right now and you want a sober assessment instead of a framework holy war: that is part of my work in Technical Consulting.

More articles

Expo over native — lessons learned from a cross-platform app Architecture · 7 min The mid-market stack, part 3: serverless in house — functions without Cloudflare Architecture · 7 min

All articles

A first conversation takes 30 minutes and comes with no obligation.

Briefly describe what you need — or book a slot right away.

or directly: hello@michaelseliger.com