Why I Chose Astro Over Next.js (And Why You Might Too)
Every developer conversation about web frameworks eventually turns into Next.js vs. [insert framework]. Next.js has won so decisively that choosing anything else feels like you need to justify it.
So here’s my justification: I chose Astro for this site, and I’d do it again.
This isn’t a Next.js hit piece. Next.js is excellent. But for content-heavy sites with selective interactivity, Astro is often the better choice. Here’s why.
The Decision Context
What I was building:
- Personal website with blog and photo gallery
- Mostly static content with occasional interactivity
- Real-time features on specific pages only
- Excellent performance and SEO as priorities
- No complex data fetching or server state
What I considered:
- Next.js (App Router)
- Next.js (Pages Router)
- Astro
- Remix
- SvelteKit
I built prototypes in Next.js and Astro. Astro won. Here’s the breakdown.
Round 1: Default JavaScript Bundle
Next.js Approach
export default function Home() { return ( <div> <h1>Welcome</h1> <p>This is a static page with no interactivity.</p> </div> );}JavaScript shipped: ~85 KB gzipped (React runtime + Next.js + hydration)
Astro Approach
---// No runtime needed for static content---
<div> <h1>Welcome</h1> <p>This is a static page with no interactivity.</p></div>JavaScript shipped: 0 KB
Winner: Astro
For pages with zero interactivity, shipping React is wasteful. Most blog posts don’t need JavaScript.
Yes, you can optimize Next.js bundles. But with Astro, zero JavaScript is the default, not something you fight for.
Round 2: Partial Hydration
Say you want one interactive component on an otherwise static page.
Next.js Approach
import { LikeButton } from './LikeButton';
export default function BlogPost() { return ( <article> <h1>My Post</h1> <p>Lots of static content...</p> <LikeButton /> {/* Hydrates React for entire page */} </article> );}JavaScript shipped: ~85 KB + your component code
Astro Approach
---import LikeButton from '../../components/LikeButton.astro';---
<article> <h1>My Post</h1> <p>Lots of static content...</p> <LikeButton client:visible /> <!-- Only hydrates this component --></article>JavaScript shipped: ~3 KB (just LikeButton, no framework)
Winner: Astro
Astro’s Islands Architecture shines here. You get granular control over what hydrates and when.
<!-- Load immediately --><Component client:load />
<!-- Load when visible --><Component client:visible />
<!-- Load when idle --><Component client:idle />
<!-- Load on media query --><Component client:media="(max-width: 768px)" />
<!-- Never hydrate, just render HTML --><Component />This level of control is what Next.js Server Components aspire to, but Astro nailed it years earlier.
Round 3: Content Collections
Both frameworks handle MDX, but the experience differs.
Next.js (App Router)
import fs from 'fs';import path from 'path';import { compileMDX } from 'next-mdx-remote/rsc';
export default async function BlogPost({ params }) { const filePath = path.join(process.cwd(), 'content', `${params.slug}.mdx`); const source = fs.readFileSync(filePath, 'utf8');
const { content, frontmatter } = await compileMDX({ source, options: { /* ... */ } });
return ( <article> <h1>{frontmatter.title}</h1> {content} </article> );}
export async function generateStaticParams() { // Manually read content directory const files = fs.readdirSync(path.join(process.cwd(), 'content')); return files.map(filename => ({ slug: filename.replace('.mdx', '') }));}Lots of manual file system operations. Feels like fighting the framework.
Astro Content Collections
---import { getCollection } from 'astro:content';
export async function getStaticPaths() { const posts = await getCollection('posts'); return posts.map(post => ({ params: { slug: post.slug }, props: { post }, }));}
const { post } = Astro.props;const { Content } = await post.render();---
<article> <h1>{post.data.title}</h1> <Content /></article>Winner: Astro
Content Collections feel purpose-built for MDX blogs. Type-safe frontmatter, automatic slug generation, built-in sorting—it all just works.
Astro’s Content Collections validate frontmatter at build time with Zod schemas. Catch mistakes before deploy.
Round 4: Build Times
This site has:
- 31 photos (optimized to 124 files)
- 4 blog posts
- 8 snippets
- 5 static pages
Next.js Build
> next build
Route (app) Size First Load JS┌ ○ / 142 B 87.2 kB├ ○ /blog 1.2 kB 88.3 kB├ ● /blog/[slug] 1.5 kB 88.8 kB├ ├ /blog/post-1├ ├ /blog/post-2└ └ ...
○ (Static) prerendered as static content● (SSG) prerendered as static HTML (uses getStaticProps)
Done in 8.4sAstro Build
> astro build
11:23:42 [build] output: "static"11:23:42 [build] directory: /dist/11:23:42 [build] Collecting build info...11:23:42 [build] ✓ Completed in 1.89s.
11:23:42 [build] Building static entrypoints...11:23:43 [build] ✓ Completed in 421ms.
11:23:43 [build] Generating static routes...11:23:43 [build] ▶ src/pages/index.astro11:23:43 [build] └─ /index.html (+15ms)...
Done in 2.3sWinner: Astro (3.6x faster)
Astro builds are consistently faster. No React runtime to bundle, fewer optimization passes, simpler architecture.
Round 5: Adding Real-Time Features
I wanted live cursors and chat on certain pages. Both frameworks support this, but the DX differs.
Next.js Client Component
'use client'; // Required for interactivity
import { useEffect, useState } from 'react';import { supabase } from '@/lib/supabase';
export function RealtimeCursors() { const [cursors, setCursors] = useState([]);
useEffect(() => { const channel = supabase.channel('cursors'); // ... realtime logic }, []);
return ( <div> {cursors.map(cursor => ( <Cursor key={cursor.id} {...cursor} /> ))} </div> );}Works fine, but every page using this component now hydrates React—even if the rest of the page is static.
Astro Island
<div id="cursors-container"></div>
<script> import { supabase } from '../lib/supabase';
const channel = supabase.channel('cursors'); // ... realtime logic (vanilla JS)</script>Or use a framework component with selective hydration:
<RealtimeCursors client:visible />Winner: Astro
Islands let you add interactivity without hydrating the entire page. Perfect for real-time features that should only load when needed.
Round 6: Developer Experience
File Routing
Both use file-based routing, but Astro feels more intuitive:
src/pages/├── index.astro → /├── about.astro → /about├── blog/│ ├── index.astro → /blog│ └── [slug].astro → /blog/:slug└── api/ └── views.ts → /api/viewsvs. Next.js App Router:
app/├── page.tsx → /├── about/│ └── page.tsx → /about├── blog/│ ├── page.tsx → /blog│ └── [slug]/│ └── page.tsx → /blog/:slug└── api/ └── views/ └── route.ts → /api/viewsThe nested page.tsx pattern feels repetitive. Astro’s .astro = page convention is cleaner.
Frontmatter vs Exports
Next.js:
export const metadata = { title: 'My Page', description: 'Description here'};
export default function Page() { return <div>Content</div>;}Astro:
---const title = 'My Page';const description = 'Description here';---
<div>Content</div>I prefer Astro’s frontmatter. It separates data from markup cleanly.
Winner: Tie (personal preference)
Round 7: Performance
Real-world Lighthouse scores for this site:
Astro Version (current)
- Performance: 99/100
- Accessibility: 100/100
- Best Practices: 100/100
- SEO: 100/100
- First Contentful Paint: 0.6s
- Largest Contentful Paint: 0.8s
- Total Blocking Time: 0ms
- Cumulative Layout Shift: 0
- Speed Index: 0.9s
Next.js Version (prototype)
- Performance: 92/100
- Accessibility: 100/100
- Best Practices: 100/100
- SEO: 100/100
- First Contentful Paint: 0.8s
- Largest Contentful Paint: 1.2s
- Total Blocking Time: 30ms
- Cumulative Layout Shift: 0
- Speed Index: 1.3s
Winner: Astro (7 points higher, 400ms faster LCP)
The React runtime adds measurable overhead. For content sites, it’s unnecessary.
When Next.js Wins
Astro isn’t always the answer. Next.js is better when:
1. Heavy Interactivity
If your site is app-like (dashboards, SaaS apps, rich interactions), Next.js wins.
// This makes sense in Next.jsexport default function Dashboard() { const [data, setData] = useState([]); const [filters, setFilters] = useState({}); const [sorting, setSorting] = useState('asc');
// Tons of client-side logic return <ComplexDataTable data={data} />;}Astro can do this, but you’re fighting the framework. Next.js embraces it.
2. Server-Side Rendering (SSR)
Astro recently added SSR, but Next.js is more mature:
// Next.js SSR is battle-testedexport default async function Page({ params }) { const data = await fetchUserData(params.id); return <Profile data={data} />;}If you need dynamic SSR for every request, Next.js has better DX.
3. API Routes with Complex Logic
Next.js API routes feel more natural for backend-heavy apps:
export async function GET(req: Request, { params }) { const user = await db.users.findById(params.id); return Response.json(user);}
export async function PATCH(req: Request, { params }) { const body = await req.json(); const updated = await db.users.update(params.id, body); return Response.json(updated);}Astro endpoints work, but Next.js feels more like Express.
4. React Ecosystem Lock-In
If you’re heavily invested in React (component libraries, custom hooks, team expertise), switching to Astro adds friction.
5. Vercel Hosting
Next.js on Vercel is magical—one-click deploy, automatic preview URLs, edge functions. It just works.
Astro works on Vercel too, but it’s optimized for Cloudflare/Netlify.
Next.js is built by Vercel. If you’re on Vercel anyway, Next.js is the path of least resistance.
When Astro Wins
Astro shines for:
1. Content-Heavy Sites
Blogs, marketing sites, documentation, portfolios—anything where content > interactivity.
2. Performance-Critical Projects
If you’re chasing perfect Lighthouse scores or optimizing for slow networks, Astro’s zero-JS default is unbeatable.
3. Multi-Framework Projects
Astro lets you mix React, Vue, Svelte, Solid in one project:
---import ReactComponent from './ReactComponent.jsx';import VueComponent from './VueComponent.vue';import SvelteComponent from './SvelteComponent.svelte';---
<div> <ReactComponent client:load /> <VueComponent client:visible /> <SvelteComponent client:idle /></div>Migrate incrementally or use the best tool for each component.
4. Learning Web Fundamentals
Astro feels closer to HTML/CSS/JS than React abstractions. Great for learning or teaching.
5. You Want Simple
Astro is easier to reason about:
.astrofiles = pages- Frontmatter = data
- Islands = interactivity
No mental model of Server Components vs Client Components.
My Specific Use Case
For this site, Astro was the right choice:
✅ Content-focused - Blog posts, photos, snippets ✅ Selective interactivity - Real-time features on some pages ✅ Performance priority - Perfect Lighthouse scores ✅ Simple architecture - Easy to understand and maintain ✅ No heavy state management - Static content with occasional API calls
Next.js would work, but I’d be fighting defaults:
- Shipping React to render markdown
- Optimizing bundles to reduce JavaScript
- Server Components mental overhead
- Unnecessary complexity for static pages
Migration Path
Already on Next.js? You don’t have to rewrite everything:
- Keep Next.js for app pages - Dashboards, interactive tools
- Move content to Astro - Blog, docs, marketing
- Share components - Astro can import React components
- Run both - Astro at
/blog, Next.js at/app
Hybrid approach is totally viable.
The Real Question
Not “Which is better?” but “Which is better for your project?”
Ask yourself:
-
Is your site mostly content or mostly app?
- Content → Astro
- App → Next.js
-
Do you need SSR for every request?
- Yes → Next.js
- No → Astro
-
Is performance critical?
- Yes → Astro
- Nice to have → Either
-
Are you deeply invested in React?
- Yes → Next.js
- No → Either
-
Do you prioritize simplicity?
- Yes → Astro
- Doesn’t matter → Either
Conclusion
I chose Astro because:
- Zero JavaScript by default - Only add what you need
- Islands Architecture - Granular hydration control
- Built for content - MDX and Content Collections feel native
- Faster builds - 2-3x faster than Next.js for this site
- Better performance - 99/100 Lighthouse without optimization
- Simpler mental model - Easier to reason about
Next.js is incredible for app-like experiences. But for content-focused sites with selective interactivity, Astro is often the better tool.
Your mileage will vary. Build a prototype in both. See which feels right for your project.
For me, Astro felt right. Three months later, no regrets.
This isn’t about tribalism. Use whatever works for you. Both frameworks are excellent at what they do.