p4ni.

Comparison

Astro vs Next.js in 2026: Which One for a Content Site?

· 10 min read

On this page

Most “Astro vs Next.js” posts compare two things that aren’t really competing. Next.js is a full-stack React framework; Astro is a content-first site framework that happens to run React. Ask “which is better” and you get a shrug. Ask “which one for a blog, docs site, marketing site, or directory” and the answer gets a lot sharper.

Here’s the short version: a default Next.js 16 page ships 642 kB of JavaScript before you write a line of feature code; this page ships 645 bytes. That gap isn’t a quality gap — it’s each framework telling you what it assumes you’re building. Below is how I measured it, what that JavaScript actually buys you, and the cases where I’d still reach for Next.js.

Where I’m coming from: I build and sell an Astro theme, so I have an obvious bias and you should read this accordingly. Two things I’d offer in exchange. Every number below comes with the command that produces it, so you can check my work. And the section on where Next.js is the better choice is the one I’d actually want you to read first.

How much JavaScript each one ships by default

Versions as of writing: Astro 7.1 (7.0 landed June 22, 2026) and Next.js 16.2 (16.0 shipped October 2025). Both had a major release in the last year and both got meaningfully faster, so anything you read from 2024 is out of date.

Comparisons like this are easy to rig, so two things about the method. I’m not comparing two finished applications — I’m comparing what each framework hands you before you write any feature code, because that’s the part you can’t opt out of later. And I’m measuring a default starter on both sides, plus a real page, so you can see both the floor and where a shipped site actually lands.

# Next.js side — then add `output: 'export'` to next.config.ts
pnpm dlx create-next-app@latest nextbaseline --ts --app --tailwind --eslint --use-pnpm --yes

# Astro side
pnpm create astro@latest astrobaseline --template minimal --install --no-git --skip-houston --yes

That gave me Next.js 16.2.12 on React 19.2.4, and Astro 7.1.4. After pnpm build on each:

Astro starterThis page (Astro)Next.js starter
External JS files008
JS shipped0645 B (inline)642 kB
Gzipped0~0.4 kB190 kB
What that JS doesTheme toggleReact runtime, router, prefetching

Measured on built output, not dev mode, on 2026-07-28. Gzip figures are gzip -9 per chunk, the way they’d actually be served. Three things I’m not counting, all of which cut against my own argument: the Next.js export also carries about 8 kB of inline script (the RSC payload); this page’s 645 bytes are compressed as part of the HTML document rather than on their own; and the analytics tag on this page — Google’s gtag, plus 293 bytes of inline setup — sits outside the 645 too. That one’s my choice rather than the framework’s, but it’s on the wire all the same.

A caveat, because this number gets quoted out of context. The Next.js figure isn’t bloat or a misconfiguration — it’s React 19.2, the App Router’s client-side router, and the prefetching machinery, all of which arrive before you’ve written anything. It’s the price of admission for an application. And this page’s 645 bytes would climb the moment I added a React island to it.

So it isn’t a quality gap. It’s a difference in what each framework assumes you’re building.

Why the gap exists: islands vs. a React runtime

Astro renders components to HTML at build time and ships no JavaScript for them at all unless you explicitly ask. That’s the islands model, and the opt-in is a single directive:

---
import PostHeader from '../components/PostHeader.astro';
import SearchBox from '../components/SearchBox.jsx';
---

<article>
  <!-- Zero JS: rendered to HTML, gone by build time -->
  <PostHeader title={post.data.title} />

  <!-- This one hydrates — React ships, but only for this component -->
  <SearchBox client:visible />
</article>

Everything is static by default; interactivity is the exception you declare. client:visible even holds the JavaScript back until the component scrolls into view.

Next.js inverts that. React Server Components mean your components can render on the server, but the App Router still boots a React runtime and a client-side router on every page so that navigations, prefetching, Suspense boundaries, and Server Actions work. Even a fully static page is a React application that happens to have nothing dynamic in it yet.

Neither default is wrong. They’re answers to different questions.

The part nobody benchmarks: getting content in

Bundle size is the argument everyone has. For a content site, the one that actually costs you time is how content gets into the site in the first place.

Astro answers it in the framework, with content collections. You define a schema once, and every file is validated at build time:

// src/content.config.ts
const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    category: z.enum(['tutorial', 'comparison', 'build-in-public']),
    draft: z.boolean().default(false),
  }),
});

A typo’d pubDate, a missing title, a category that isn’t in the list — all of it fails the build instead of shipping, and getCollection('blog') comes back fully typed. That typed frontmatter is also what makes emitting correct JSON-LD a mapping exercise rather than a string-template one.

Next.js has no equivalent in the framework. You wire up MDX yourself and add the type-safe layer on top, which means picking a third-party dependency: Contentlayer, the tool most people reached for, hasn’t shipped a release since June 2023, leaving a community fork (contentlayer2) and a spiritual successor (Content Collections) as the live options. It’s all very doable — it’s just yours to assemble and maintain.

That’s the same pattern as the bundle-size gap, one level up. Astro assumes content is the primary object; Next.js assumes it’s data you happen to be rendering.

What Next.js gives you for that JavaScript

The other side of the ledger is real, and Next.js 16 is a genuinely strong release:

  • Cache Components — the "use cache" directive plus Partial Prerendering, so one route can serve a static shell instantly and stream the personalized parts in. Caching is now entirely opt-in rather than the implicit behavior that confused everyone in Next.js 13–14. Enable it with cacheComponents: true in next.config.ts.
  • Server Actions — mutations without hand-writing an API layer.
  • proxy.tsmiddleware.ts renamed, now running on the Node.js runtime, with the old filename deprecated. Request interception for auth gates, rewrites, geo routing.
  • ISR and on-demand revalidationrevalidateTag() and the new updateTag() for read-your-writes semantics.
  • The React ecosystem, unqualified — every component library, every hook, no integration layer.

If your project needs a logged-in dashboard, per-user rendering, or forms that write to a database, that 190 kB is buying you something real, and Astro would make you assemble the equivalent yourself.

The catch: as a static export, you give most of it back

Here’s the part that decides most content-site arguments. If you deploy Next.js as a static export — output: 'export', which is what you’d do to host a blog on a CDN — there’s no server at runtime, and most of that list goes with it. Per the Next.js docs:

  • no proxy.ts / middleware
  • no Server Actions
  • no ISR or on-demand revalidation
  • no redirects, rewrites, or headers in next.config.ts — your host has to handle them
  • every dynamic route must export generateStaticParams(), and dynamicParams has to stay false; leaving it unset is fine, but writing export const dynamicParams = true fails the build
  • no built-in image optimization (you need unoptimized: true or a custom loader)

You keep the 190 kB and give up the reasons for it.

The obvious response is: don’t use static export. Deploy Next.js with a server runtime — on Vercel that’s the default path, it’s free at this scale, and you keep ISR, Server Actions, and image optimization. That’s a legitimate choice, and if you’re already on that platform I wouldn’t argue you off it.

What it doesn’t change is the client side. Server-rendered or not, the browser still boots the React runtime and the App Router before the page is interactive, on every visit. For a dashboard, that cost is amortized across a long session. For a blog post someone opens once from search on a mid-range Android phone, you pay it in full and get nothing back. That’s the actual trade — not “Next.js is slow”, but “you’re paying an application framework’s startup cost for a document that gets read once and closed.”

Where Next.js is the better choice

Stated as plainly as I can:

  • App-shaped UI. If most screens are interactive — a builder, an editor, a dashboard with cross-component state — you’ll end up with islands everywhere, and islands don’t share state easily. At that density, Next.js is the simpler architecture. Astro isn’t trying to win here.
  • Client-side routing. Astro navigations are full page loads by default. Astro’s <ClientRouter /> opts you into client-side routing and transition:persist keeps island state across navigations, so this is closer than it used to be — but Next.js’s router does more out of the box and the SPA-heavy patterns are far better trodden there.
  • Auth-gated content. Doable with Astro’s server output and an adapter, but Next.js has the more paved road here, and far more has been written about it.
  • Team familiarity. A React team ships faster in Next.js on day one. .astro files are easy — they’re mostly HTML — but they’re one more thing to learn.

There’s also an honest middle ground: Astro renders React, Vue, Svelte, Solid, and Preact components with a one-line official integration, so “we’re a React shop” isn’t disqualifying. You write React for the interactive pieces and .astro for the pages.

Build speed: no longer a real differentiator

Both frameworks spent the last year here. Astro 7 rewrote the compiler and the Markdown pipeline in Rust and moved to Vite 8 with Rolldown, putting published gains at 15–61% — developers.cloudflare.com (8,431 pages) went from 386.9s to 261.9s. Next.js 16 made Turbopack the default bundler for dev and production, claiming 2–5× faster production builds.

Both are fast enough that it shouldn’t decide anything. This site builds 35 pages in about 13 seconds, and roughly 80% of that is Satori generating an OG image per post — Astro’s own share is a couple of seconds. Build speed isn’t the only thing Astro 7 changed, though: the same release also started detecting AI coding agents and backgrounding astro dev, which is how a dev server ended up running on my machine for 26 hours.

Which one for your site

If your site is…Pick
Blog, docs, marketing site, changelogAstro
Directory or catalog, browse-heavy, mostly readAstro
Content site with a few interactive widgetsAstro (islands)
Content site + logged-in area on the same domainNext.js, or Astro with a server adapter
Dashboard, editor, anything app-shapedNext.js
Team is React-native and the deadline is FridayNext.js

Two shortcuts that hold up well in practice:

Count the interactive components on a typical page. Zero to three: Astro, easily. A dozen that talk to each other: Next.js.

Ask whether you need a server at runtime. If the honest answer is no, Astro lets you act on that — the output is a folder of files you can put on any static host. I deploy this site to Cloudflare Workers static assets, and the whole deploy step is astro build && wrangler deploy.

The bottom line

Use Astro when the content is the product. Use Next.js when the application is the product. The 645 B vs. 642 kB gap isn’t a scorecard — it’s the two frameworks telling you what they think you’re building, and it’s worth listening to.

Where this gets interesting is the projects that start as content and grow into applications. A directory site is the clearest example: browsing is pure content, but submissions, search, and an admin panel are application work. You can serve both from Astro — static pages plus a server runtime for the dynamic routes — which is how Almanac works on Cloudflare Workers and D1. I’ve written up how to build that stack from scratch, and compared the directory themes worth using if you’d rather not.