DevLift
Back to Blog

Astro vs Next.js: When Content-First Architecture Actually Matters

Astro's JavaScript floor is zero; Next.js's is a framework baseline. That one architectural difference — hydration opt-in vs opt-out — decides most of this comparison, and it matters much less than it sounds once your users are logged in.

Admin
June 17, 20268 min read2 views

Astro vs Next.js: When Content-First Architecture Actually Matters

Your company is rebuilding its documentation site and marketing pages. The engineering team already uses Next.js for the main SaaS app. The obvious move is to use it for docs too — same team, same tooling, no context switching. Then someone runs Lighthouse against your competitor's Astro-powered docs site, and against yours, and the two numbers are not close. Same content, different framework.

That's when the debate actually starts.

Quick Decision Matrix

SituationUse
Blog, docs, marketing site, content-firstAstro
SaaS dashboard, authenticated app, complex stateNext.js
Mostly static with occasional personalizationAstro + Server Islands
E-commerce with catalog, checkout, and accountsNext.js
Multi-framework team (React + Vue + Svelte)Astro
Heavy existing Next.js codebaseNext.js (migration cost > performance gain)
Need fine-grained ISR and cache revalidationNext.js
Static CDN deploy, zero infra costsAstro

Where the JavaScript Difference Actually Comes From

You will find a lot of side-by-side kilobyte numbers for this comparison. I'm not repeating any of them, because none of the ones in circulation say what they built, what was on the page, or whether compression was counted — and the number is entirely determined by those choices. The structural claim underneath them doesn't need a benchmark, though, and it's the part that should drive your decision: in Next.js, hydration is opt-out; in Astro, it's opt-in.

A Next.js App Router page ships a framework baseline — the React runtime, the client-side router, the hydration and RSC-payload machinery — on every page, before any of your components are counted. That baseline is not zero even for a page with no interactive components.

Astro's default output is static HTML. No JavaScript runtime, no client-side router, no hydration baseline. If a page has no client:* directives, the browser gets zero bytes of JS. That's the default, not an optimization mode you configure.

The difference that follows is therefore not a constant factor, it's a floor: Astro's floor is zero and Next.js's is the framework baseline. On a docs page with three interactive widgets, that gap is most of the total. On a dashboard where everything is interactive, it's rounding error. If you want your own figure, build the same page both ways and read the network panel — it takes an afternoon and the result is about your site rather than someone's demo.

The Interactivity Models: Islands vs RSC

This is where the real conceptual difference lives.

Astro's Island Architecture treats interactive components as explicit opt-in islands within an otherwise static HTML page. You write a component — React, Vue, Svelte, Solid, or Preact — and hydration is controlled by a directive:

---
// BlogPost.astro — this code runs at build time (or request time for SSR)
const { slug } = Astro.params;
const post = await getPost(slug);
---
 
<article>
  <h1>{post.title}</h1>
  <div set:html={post.content} />
 
  <!-- Zero JS — pure static HTML -->
  <RelatedPosts posts={post.related} />
 
  <!-- Island: hydrates only when scrolled into view -->
  <CommentSection postId={post.id} client:visible />
 
  <!-- Island: hydrates immediately (above the fold) -->
  <ShareButtons url={post.url} client:load />
 
  <!-- Island: hydrates when browser is idle -->
  <NewsletterSignup client:idle />
</article>

Each client:* directive is an independent island. They don't share a runtime. CommentSection can be a React component, ShareButtons can be a Svelte component, and they coexist on the same page without conflict.

Next.js uses React Server Components. The server renders a component tree; components marked with "use client" send their JavaScript to the browser and hydrate. Without that directive, they stay on the server permanently.

// app/blog/[slug]/page.tsx — Server Component by default
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;                // params is a Promise since Next.js 15
  const post = await getPost(slug);             // runs on server, never hits the client
 
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
      <RelatedPosts posts={post.related} />  {/* Server Component — no JS */}
      <CommentSection postId={post.id} />    {/* Client Component — ships JS */}
      <ShareButtons url={post.url} />        {/* Client Component — ships JS */}
    </article>
  );
}
// components/CommentSection.tsx
"use client"; // opts this entire subtree into the client bundle
 
import { useState } from "react";
 
export function CommentSection({ postId }: { postId: string }) {
  const [comments, setComments] = useState<Comment[]>([]);
  // ...
}

The critical difference: in Next.js, all "use client" components share the React runtime — it ships once and everything uses it. In Astro, if you mix React and Svelte islands, both runtimes ship separately — but only for components that actually need them.

Dynamic Content Without Giving Up the Performance Baseline

Pure SSG breaks when you need personalization. Both frameworks solved this with different approaches that landed around the same time.

Astro Server Islands (stable since Astro 5) let specific components render on demand per-request while the rest of the page stays fully static. The one prerequisite the examples usually omit: server islands need an adapter installed to do the deferred rendering, so a pure output: 'static' build with no adapter won't produce them.

---
import ArticleLayout from '../layouts/ArticleLayout.astro';
import UserRecommendations from '../components/UserRecommendations.jsx';
---
 
<ArticleLayout>
  <!-- Entire page serves from CDN — static, instant -->
  <slot />
 
  <!-- This component renders on the server for each request -->
  <!-- Page shell doesn't wait for it to finish -->
  <UserRecommendations server:defer>
    <div slot="fallback" class="skeleton-loader" />
  </UserRecommendations>
</ArticleLayout>

The page shell hits the browser from CDN immediately. The server island streams in afterward. No SSR penalty on the whole page.

Next.js Partial Prerendering achieves the same outcome with a different mental model — Suspense boundaries become the split point. One naming trap: in Next.js 16 (released 21 October 2025) the experimental.ppr flag is gone. PPR shipped as part of Cache Components, so the switch is cacheComponents: true in next.config.ts, and turning it on also changes the rest of your caching model — the dynamic, revalidate and runtime segment configs are rejected once it's enabled, and caching moves to the use cache directive. It is not a one-line opt-in on an existing app.

// app/blog/[slug]/page.tsx
import { Suspense } from "react";
 
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
 
  return (
    <article>
      {/* Static shell — served from CDN instantly */}
      <StaticArticleContent slug={slug} />
 
      {/* Dynamic — streams from origin after the static shell lands */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <PersonalizedRecommendations />
      </Suspense>
    </article>
  );
}
💡
In PPR, Next.js infers what's static vs dynamic automatically. Any component that calls cookies(), headers(), or searchParams forces dynamic rendering for that subtree. In Astro, you explicitly mark components with server:defer.

Data Fetching: Two Different Mental Models

In Astro, data fetching in .astro files runs on the server — always. At build time for SSG, at request time for SSR. Either way, your API keys and private env vars never touch the browser:

---
// This code only runs on the server — never shipped to the client
const apiKey = import.meta.env.PRIVATE_API_KEY; // genuinely safe
 
const [posts, featured] = await Promise.all([
  fetch(`${import.meta.env.API_URL}/posts`).then(r => r.json()),
  fetch(`${import.meta.env.API_URL}/featured`).then(r => r.json()),
]);
---
 
<main>
  <FeaturedPost post={featured} />
  <PostGrid posts={posts} />
</main>

In Next.js, data fetching is tied to the RSC model, with an ISR layer baked into fetch():

// app/posts/page.tsx — Server Component
import { cache } from "react";
 
// React.cache deduplicates calls across component tree during same request
const getPosts = cache(async () => {
  const res = await fetch(`${process.env.API_URL}/posts`, {
    next: { revalidate: 3600 }, // ISR: regenerate at most once per hour
  });
  return res.json();
});
 
export default async function PostsPage() {
  const posts = await getPosts();
  return <PostGrid posts={posts} />;
}

Next.js's revalidateTag() for on-demand ISR is genuinely powerful for large content sites where you need to invalidate specific pages when CMS content updates. Astro doesn't have this natively — you'd need to trigger a rebuild or rely on edge cache headers.

Multi-Framework Support: The Feature Nobody Talks About Enough

Astro renders React, Vue, Svelte, Solid, and Preact components on the same page:

---
import ReactSearchBar from './SearchBar.tsx';    // React component
import VueAnalyticsChart from './Chart.vue';      // Vue component
import SvelteCounter from './Counter.svelte';      // Svelte component
---
 
<ReactSearchBar client:load />
<VueAnalyticsChart client:visible />
<SvelteCounter client:idle />

For teams mid-migration between frameworks, or for companies pulling in components from multiple codebases, this is real practical value — not just a party trick. You can incrementally move from Vue to React by writing new components in React while keeping existing Vue components working in the same pages.

Next.js is React-only. There's no path to mixing frameworks.

Architecture Comparison

Rendering diagram...

When to Use Astro

Documentation sites and developer portals: Astro's Starlight theme is the default choice for serious open-source docs. Zero-JS baseline, instant page transitions via view transitions API, excellent Markdown and MDX support, and content collections that give you type-safe frontmatter.

Marketing and landing pages: a sales page doesn't need a React runtime at all. It needs fast loads and good Core Web Vitals for SEO. Astro's defaults get you there without configuration.

Content-heavy blogs and publishing: If your workflow is MDX articles, Astro's content collections with Zod schema validation are genuinely cleaner than any Next.js MDX setup. Content is first-class, not bolted on.

Teams experimenting with multiple frontend frameworks: The ability to run React alongside Vue or Svelte without conflict is useful during migrations or when pulling in third-party component libraries from different ecosystems.

Strict hosting cost constraints: a static Astro build is a directory of files, so it deploys to any CDN and the cheapest tier of most of them covers a content site. Cloudflare announced in January 2026 that the Astro team was joining Cloudflare, with Astro staying open source, so expect the Cloudflare integration to keep getting the most attention — check the current Astro major before you follow any tutorial, though. The line moves fast: Astro 6 was in beta at the time of that announcement and the current stable is already 7.x.

When to Use Next.js

SaaS dashboards and complex authenticated apps: Once you're managing sessions, real-time updates, optimistic mutations, and hundreds of interactive components, Astro's opt-in interactivity model starts feeling like friction. RSC, Server Actions, and the React ecosystem are built for this.

E-commerce with product catalogs: ISR with revalidateTag() is the practical solution for keeping 50,000 product pages fresh when inventory changes. You can trigger targeted revalidation from your CMS webhook. Astro requires a full rebuild or custom edge cache tooling to get there.

Full-stack apps with colocated backend logic: Next.js Server Actions let you write database mutations in the same file as the form component that triggers them. Route Handlers give you an ergonomic API layer. Astro has API endpoints, but they're not the center of the framework's design.

Existing heavy investment in React: If your team has hundreds of React components, complex state management, and React-specific libraries (react-query, react-hook-form, Zustand), there's no practical reason to move to Astro for the framework's sake. The migration cost exceeds the performance gain for most applications.

When to Use Both

This is more common than it sounds, and it works well: Next.js for the authenticated product, Astro for the marketing site and docs. Different domains, different deployment targets, different optimization goals.

The development setup with both in a monorepo (Turborepo or Nx) isn't painful. Shared design system components live in a package and get imported by both — Astro can consume any React component library, so sharing a component package with your Next.js app works out of the box.


The choice isn't about which framework is generally better. It's about whether your project's primary need is serving content to anonymous visitors at maximum speed, or providing interactive application functionality to authenticated users. The performance gap for content sites is real and measurable. The ecosystem and ergonomics advantage for complex apps is also real.

If you're building something that's mostly articles, docs, or marketing pages — Astro's defaults will serve you better than any amount of Next.js optimization. If you're building something where most users are logged in, doing things, and interacting with complex state — Next.js is the more natural fit, and you won't feel like you're working against the framework.

Comments (0)

No comments yet. Be the first to share your thoughts!

Related Articles

One file to guard every route — plus the Next.js 16 rename that moves it off the Edge runtime, the request-vs-response header trap that leaks user IDs to the browser, and the CVE that explains why this can never be your only auth layer.
AdminAugust 3, 20268 min read
Server Actions collapse form handling into a single server function—no API route, no manual fetch, no useState for loading. Here's the pattern that replaces 80% of your mutation boilerplate.
AdminAugust 3, 20267 min read
A monotonic stack maintains elements in order and pops when that order breaks — finding the next greater element for every popped value in O(n) total.
AdminAugust 3, 20265 min read