Three frameworks, three philosophies. Next.js is the React full-stack powerhouse. Remix (now React Router v7) is the progressive-enhancement idealist. Astro is the content-site speed champion that ships zero JavaScript by default. This guide breaks down exactly when to use each.
At a glance
| Next.js 15 | Remix / React Router v7 | Astro 5 | |
|---|---|---|---|
| Released | 2016 | 2021 (Remix), 2024 (RR v7) | 2021 |
| Primary use case | Full-stack React apps, e-commerce, SaaS | Data-driven apps, forms, progressive enhancement | Content sites, blogs, marketing, documentation |
| UI framework | React only | React only | Any (React, Vue, Svelte, Solid, or none) |
| Default rendering | SSR + RSC (App Router) | SSR | SSG / MPA |
| JS shipped by default | Runtime JS bundle | Runtime JS bundle | ~0 KB (islands on demand) |
| Routing | File-system (app/ or pages/) | File-system (nested layouts) | File-system (src/pages/) |
| Data fetching | fetch() in RSC, Server Actions | loader/action functions | Astro.props, Content Collections, loaders |
| Backend | Route Handlers, Server Actions | Remix loaders/actions | Endpoints (.ts files) |
| Learning curve | Medium → High (App Router) | Medium | Low → Medium |
| Backed by | Vercel | Shopify (open source) | The Astro team |
| GitHub stars | ~130k | ~30k (Remix) | ~50k |
How each framework works
Next.js: The full-stack Swiss Army knife
Next.js wraps React with a file-system router, two rendering modes (App Router with React Server Components, and the legacy Pages Router), and a suite of server features. The App Router (default since Next.js 13) treats components as server-rendered by default — they run on the server, access databases directly, and send HTML+RSC payload to the client.
// app/posts/[slug]/page.tsx — Server Component (no 'use client')
import { db } from '@/lib/db'
export default async function PostPage({ params }: { params: { slug: string } }) {
// Direct DB access — runs on the server only
const post = await db.post.findUnique({ where: { slug: params.slug } })
return <article>{post?.content}</article>
}
// Server Action — form submitted without a separate API endpoint
async function likePost(formData: FormData) {
'use server'
const postId = formData.get('postId') as string
await db.post.update({ where: { id: postId }, data: { likes: { increment: 1 } } })
}
Next.js strengths:
- Largest ecosystem (most tutorials, plugins, community answers)
- Native Vercel deployment (zero-config Edge, ISR, image optimisation)
- React Server Components — colocate DB queries with UI
next/image,next/font,next/linkfor performance- ISR (Incremental Static Regeneration) — pages rebuild in background
Remix / React Router v7: Progressive enhancement done right
Remix merged with React Router in late 2024. React Router v7 (previously Remix v2) introduces "framework mode" — you get Remix's loader/action model directly in the React Router package. The philosophy: forms work without JavaScript, and you build on top of web standards (Request/Response, FormData, URLSearchParams).
// app/routes/posts.$slug.tsx — loader fetches, action handles mutations
import { data, Form, useLoaderData } from 'react-router'
import { db } from '~/lib/db'
import type { Route } from './+types/posts.$slug'
export async function loader({ params }: Route.LoaderArgs) {
const post = await db.post.findUnique({ where: { slug: params.slug } })
if (!post) throw data('Not Found', { status: 404 })
return { post }
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData()
await db.post.update({
where: { slug: params.slug },
data: { likes: { increment: 1 } },
})
return { ok: true }
}
export default function PostRoute({ loaderData }: Route.ComponentProps) {
const { post } = loaderData
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* Works without JavaScript — native form */}
<Form method="post">
<input type="hidden" name="intent" value="like" />
<button type="submit">Like</button>
</Form>
</article>
)
}
Remix strengths:
- Progressive enhancement — works without JS, enhanced with JS
- Nested routes with independent loaders (no waterfall fetching)
- Full-stack mutations via actions (no separate API layer)
- Automatic error boundaries at route level
- Runs on any runtime: Node.js, Cloudflare Workers, Deno, Bun
Astro: Ship less JavaScript, rank higher
Astro is an MPA (Multi-Page App) framework with an islands architecture. Pages are .astro files (HTML-like templates with frontmatter scripts) that render to static HTML by default. JavaScript is opt-in via client:* directives on interactive components — from any UI framework.
---
// src/pages/posts/[slug].astro — runs at build time (SSG) or request time (SSR)
import { getEntry, render } from 'astro:content'
import Layout from '@/layouts/BaseLayout.astro'
const { slug } = Astro.params
const post = await getEntry('blog', slug!)
const { Content } = await render(post)
---
<Layout title={post.data.title}>
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
<!-- Interactive island — loads React only for this component -->
<LikeButton client:load postId={post.id} />
</Layout>
---
// Using multiple UI frameworks in one project
import ReactCounter from '@/components/Counter.tsx' // React
import VueSearch from '@/components/Search.vue' // Vue
import SvelteChart from '@/components/Chart.svelte' // Svelte
---
<!-- Each island is independently hydrated -->
<ReactCounter client:visible />
<VueSearch client:idle />
<SvelteChart client:media="(min-width: 768px)" />
Astro strengths:
- Zero JS by default — perfect Lighthouse scores on content pages
- Framework-agnostic — use React, Vue, Svelte, or plain HTML
- Content Collections with type-safe frontmatter
- View Transitions API for SPA-like navigation
- Built-in Markdown + MDX support
Rendering strategies compared
| Strategy | Next.js | Remix / RR v7 | Astro |
|---|---|---|---|
| SSG (build time) | generateStaticParams + no dynamic |
ssr: false adapter mode |
Default (no adapter) |
| SSR (per request) | Default in App Router | Default | With @astrojs/node or Cloudflare adapter |
| ISR | revalidate in fetch / next.revalidate |
❌ (not built-in) | Experimental server:defer |
| RSC (React Server Components) | ✅ App Router | ❌ | ❌ |
| Streaming | ✅ Suspense boundaries | ✅ defer() in loaders |
✅ Experimental |
| Edge rendering | ✅ runtime: 'edge' |
✅ Cloudflare Workers | ✅ Any adapter |
| Partial hydration | ❌ | ❌ | ✅ Islands (client:*) |
Routing and nested layouts
All three use file-system routing, but with different conventions:
| Feature | Next.js | Remix / RR v7 | Astro |
|---|---|---|---|
| Convention | app/ (App Router) or pages/ |
app/routes/ |
src/pages/ |
| Dynamic segment | [slug] |
$slug |
[slug] |
| Catch-all | [...slug] |
$ (splat) |
[...slug] |
| Nested layouts | layout.tsx per folder |
Dot-separator routes | Layout components (manual) |
| Parallel routes | @folder slots |
❌ | ❌ |
| Intercepting routes | (.)route |
❌ | ❌ |
| Loading state | loading.tsx |
useNavigation() |
❌ (full page) |
| Error boundary | error.tsx |
ErrorBoundary export |
❌ (manual) |
# Next.js App Router structure
app/
layout.tsx ← root layout
page.tsx ← /
posts/
layout.tsx ← wraps all /posts/* routes
page.tsx ← /posts
[slug]/
page.tsx ← /posts/my-post
loading.tsx ← skeleton while fetching
error.tsx ← error boundary
# Remix / React Router v7 structure
app/routes/
_index.tsx ← /
posts._index.tsx ← /posts (dot = nested)
posts.$slug.tsx ← /posts/my-post
# Astro structure
src/pages/
index.astro ← /
posts/
index.astro ← /posts
[slug].astro ← /posts/my-post
Data fetching patterns
// Next.js — Server Component with direct DB access
async function PostList() {
const posts = await db.post.findMany({ orderBy: { date: 'desc' } })
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
// Next.js — cached fetch with revalidation
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // ISR: rebuild every hour
})
return res.json()
}
// Server Action — client mutation without API route
'use server'
async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.post.create({ data: { title } })
revalidatePath('/posts')
}
// Remix / React Router v7 — loader runs before render
export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url)
const page = Number(url.searchParams.get('page') ?? 1)
const posts = await db.post.findMany({ skip: (page - 1) * 10, take: 10 })
return { posts, page }
}
// Parallel data fetching — no waterfall
export async function loader() {
const [posts, categories] = await Promise.all([
db.post.findMany(),
db.category.findMany(),
])
return { posts, categories }
}
// Deferred (stream expensive data)
import { defer } from 'react-router'
export async function loader() {
return defer({
posts: db.post.findMany(), // awaited immediately
analytics: getSlowAnalytics(), // streamed in background
})
}
---
// Astro — frontmatter runs on server (build or request time)
import { getCollection } from 'astro:content'
const posts = await getCollection('blog', ({ data }) => !data.draft)
const sorted = posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
---
{sorted.map(post => (
<article>
<a href={`/posts/${post.slug}`}>{post.data.title}</a>
<time>{post.data.date.toLocaleDateString()}</time>
</article>
))}
Performance compared
| Metric | Next.js | Remix / RR v7 | Astro |
|---|---|---|---|
| Initial HTML | Server-rendered (SSR/RSC) | Server-rendered | Server-rendered |
| JS bundle (hello world) | ~90 KB (React + framework) | ~75 KB (React + RR) | ~0 KB |
| JS bundle (real app) | 150–400 KB | 120–300 KB | 10–60 KB (islands only) |
| Time to Interactive | Medium (hydration cost) | Medium (hydration cost) | Fast (minimal hydration) |
| Core Web Vitals (content) | Good | Good | Excellent |
| Core Web Vitals (SaaS) | Good | Good | Good (if using React islands) |
| Image optimisation | ✅ next/image built-in |
Manual (via adapter) | ✅ <Image /> built-in |
| Font optimisation | ✅ next/font |
Manual | ✅ @astrojs/font |
Real-world bundle audit (blog homepage):
- Next.js App Router: ~94 KB JS (gzip)
- Remix: ~72 KB JS (gzip)
- Astro (no islands): ~4 KB JS (gzip)
- Astro (React island for search): ~52 KB JS (gzip, loads lazily)
Ecosystem and integrations
Next.js ecosystem
| Category | Options |
|---|---|
| Auth | NextAuth.js (Auth.js), Clerk, Supabase Auth, Lucia |
| Database ORM | Prisma, Drizzle, Supabase, Mongoose |
| UI libraries | shadcn/ui, Radix UI, Chakra, MUI, Ant Design |
| State management | Zustand, Jotai, Redux Toolkit, TanStack Query |
| Styling | Tailwind CSS, CSS Modules, styled-components, Stitches |
| Testing | Vitest, Jest, Playwright, Cypress, Testing Library |
| CMS | Sanity, Contentful, Payload, Strapi, Builder.io |
| Deploy | Vercel (native), AWS, Railway, Fly.io, Render |
| E-commerce | Medusa, Commerce.js, Shopify Hydrogen (moved to Remix) |
| Analytics | Vercel Analytics, PostHog, Plausible |
Remix / React Router v7 ecosystem
| Category | Options |
|---|---|
| Auth | Remix Auth, better-auth, Clerk |
| Database ORM | Prisma, Drizzle (great fit for Cloudflare D1) |
| UI libraries | Any React UI library |
| Styling | Tailwind CSS, Vanilla Extract, CSS Modules |
| Deploy | Cloudflare (Workers/Pages), Fly.io, Vercel, Railway |
| Testing | Vitest + Testing Library, Playwright |
| Community | Smaller than Next.js; strong Cloudflare community |
Astro ecosystem
| Category | Options |
|---|---|
| UI frameworks | React, Vue, Svelte, Solid, Preact, Alpine.js, Lit |
| Content | Markdown, MDX, Content Collections (built-in) |
| CMS | Storyblok, Sanity, Contentful, Ghost, Decap |
| Styling | Tailwind CSS, CSS Modules, scoped <style> per component |
| Database | Any (via API routes or Astro DB — built-in SQLite/libSQL) |
| Deploy | Cloudflare Pages/Workers, Vercel, Netlify, Node.js |
| Testing | Playwright (E2E), Vitest for utils |
| Themes | Rich official + community theme ecosystem |
| Integrations | 100+ official integrations (npx astro add tailwind) |
Where each framework wins
Next.js wins for
| Scenario | Why |
|---|---|
| SaaS dashboards | RSC + Server Actions = minimal API boilerplate |
| E-commerce | ISR + next/image + Commerce integrations |
| Large teams | Biggest ecosystem, most StackOverflow answers |
| Vercel-deployed apps | Zero-config ISR, Edge Middleware, Analytics |
| AI/LLM apps | AI SDK, streaming responses, Vercel AI integrations |
| When you want maximum options | Pages Router or App Router, RSC or not |
| SEO-critical apps | RSC + ISR + automatic metadata API |
Remix / React Router v7 wins for
| Scenario | Why |
|---|---|
| Complex forms and mutations | Loader/action mental model reduces boilerplate |
| Progressive enhancement required | Works without JS — government, accessibility-first sites |
| Cloudflare Workers | Best-in-class Worker adapter |
| Nested layouts with isolated data | Each route loads its own data independently |
| Existing React Router v6 apps | Migration path via framework mode |
| Clean HTTP semantics | Respects Cache-Control, status codes, web standards |
| Real-time feedback (optimistic UI) | useFetcher + optimistic updates built in |
Astro wins for
| Scenario | Why |
|---|---|
| Blogs and documentation | Zero JS, perfect Core Web Vitals |
| Marketing / landing pages | Fast by default, great SEO |
| Multi-framework teams | React devs + Vue devs can coexist |
| Content-heavy sites | Content Collections = type-safe MDX management |
| Portfolio sites | Beautiful theme ecosystem, fast builds |
| When you need to migrate incrementally | Islands let you add React bit by bit |
| Lighthouse score requirements | 100/100 achievable without effort |
Full comparison table
| Feature | Next.js 15 | Remix / RR v7 | Astro 5 |
|---|---|---|---|
| React required | ✅ | ✅ | Optional |
| TypeScript support | Excellent | Excellent | Excellent |
| RSC (React Server Components) | ✅ | ❌ | ❌ |
| SSR | ✅ | ✅ | ✅ (with adapter) |
| SSG | ✅ | ✅ | ✅ (default) |
| ISR | ✅ | ❌ | Experimental |
| Zero JS by default | ❌ | ❌ | ✅ |
| Island architecture | ❌ | ❌ | ✅ |
| Nested layouts | ✅ (layout.tsx) | ✅ (dot routes) | Manual |
| Built-in form handling | Server Actions | loader/action | Endpoints |
| File uploads | Server Actions | action + FormData | Endpoints |
| Middleware | ✅ (Edge) | ✅ | ✅ |
| Image optimisation | ✅ next/image | Manual | ✅ |
| Font optimisation | ✅ next/font | Manual | ✅ |
| Built-in CSS Modules | ✅ | ✅ | ✅ |
| Tailwind support | ✅ | ✅ | ✅ |
| MDX support | Plugin | Plugin | ✅ built-in |
| Content Collections | ❌ | ❌ | ✅ built-in |
| View Transitions | Manual | Manual | ✅ built-in |
| API routes | Route Handlers | Resource routes | Endpoints |
| WebSocket support | Limited | Cloudflare Durable | Limited |
| Cloudflare Workers | ✅ (via adapter) | ✅ (best support) | ✅ |
| Edge runtime | ✅ | ✅ | ✅ |
| Storybook support | ✅ | ✅ | ✅ |
| Docker deploy | ✅ | ✅ | ✅ |
| Standalone output | ✅ output: standalone |
✅ | ✅ |
| Bundle analyser | @next/bundle-analyzer |
Manual | Manual |
| DevTools | ✅ | Remix DevTools | ✅ |
| Stable API | ✅ | ✅ | ✅ |
| Community size | ★★★★★ | ★★★ | ★★★★ |
| Job market | ★★★★★ | ★★★ | ★★★ |
Migration paths
| From → To | Effort | Notes |
|---|---|---|
| Pages Router → App Router (Next.js) | Medium | Incremental — both coexist |
| React Router v6 → React Router v7 (framework mode) | Low–Medium | Official migration guide |
| CRA / Vite → Next.js | Medium | Add file-system routing, adjust data fetching |
| Next.js Pages Router → Astro | Medium | Replace getStaticProps with frontmatter fetching |
| Next.js App Router → Astro | High | Different data model (RSC → islands) |
| Gatsby → Astro | Low | Both are SSG-first; GraphQL layer removed |
| WordPress → Astro | Low–Medium | Astro + headless WP via REST or WPGraphQL |
Decision guide
Do you need React specifically?
├── No → Astro (use any framework or none)
└── Yes →
Is this a content/marketing/blog site?
├── Yes → Astro (React islands where needed)
└── No (app / SaaS / e-commerce) →
Do you care about progressive enhancement / Cloudflare?
├── Yes → Remix / React Router v7
└── No → Next.js
Still unsure?
- Largest team / most hiring? → Next.js
- Best SEO with minimal JS? → Astro
- Web standards / edge-first? → Remix / React Router v7
Common mistakes
| Mistake | Framework | Fix |
|---|---|---|
Adding 'use client' to every component |
Next.js | Only client-ify leaf components that need browser APIs or events |
Fetching in useEffect instead of loaders |
Remix | Move fetching to loader — that's the whole point |
| Installing React in an Astro project when plain HTML/CSS suffices | Astro | Use .astro components; only add React for interactive islands |
Using getServerSideProps in App Router |
Next.js | App Router = async Server Components; no getServerSideProps |
Using <Link> from react-router in framework mode without the adapter |
Remix / RR v7 | Import from react-router, not react-router-dom |
| Shipping a 300 KB React bundle for a static blog | Astro | Astro = 0 KB JS for static content |
Forgetting that loader runs on every navigation |
Remix | Use caching headers (Cache-Control) if loader is slow |
Not configuring output: 'standalone' for Docker |
Next.js | Required to avoid shipping node_modules in Docker image |
Next.js vs Remix vs Astro vs SvelteKit
| Next.js | Remix / RR v7 | Astro | SvelteKit | |
|---|---|---|---|---|
| UI language | React / TSX | React / TSX | Any / .astro |
Svelte |
| Bundle size | Large | Large | Tiny | Small |
| RSC support | ✅ | ❌ | ❌ | ❌ |
| Zero JS | ❌ | ❌ | ✅ | ❌ |
| Form mutations | Server Actions | loader/action | Endpoints | form actions |
| Deploy anywhere | ✅ | ✅ | ✅ | ✅ |
| Community | ★★★★★ | ★★★ | ★★★★ | ★★★★ |
| Best for | Full-stack React | Progressive web apps | Content sites | Svelte full-stack |
FAQ
Q: Is Remix dead now that it merged with React Router?
No. Remix v2 features are now in React Router v7 under the "framework mode" (react-router.config.ts). The Remix team works on React Router v7. Existing Remix v2 apps migrate with minimal changes — mostly updating import paths. The brand "Remix" is being sunset in favour of "React Router", but the technology is very much alive and actively developed.
Q: Can I use Astro for a full SaaS app (not just content)?
Yes, but you'll write most of the app as interactive React/Vue/Svelte islands, which reduces Astro's main advantage (zero JS). For a dashboard or SaaS app with lots of client interactivity, Next.js or Remix will feel more natural. Astro's sweet spot is 80%+ static content with sprinkles of interactivity.
Q: Next.js App Router vs Pages Router — which should I use in 2025?
App Router for new projects. It's the Vercel-recommended default with React Server Components, built-in streaming, and Server Actions. Pages Router is stable and won't be removed, but receives minimal new features. The App Router learning curve is steeper (understanding RSC boundaries), but worth it for new projects.
Q: Which is best for SEO?
All three render HTML server-side, so search crawlers see content immediately. Astro ships the least JavaScript, which helps Core Web Vitals (especially INP and TBT). Next.js with ISR serves pre-built HTML instantly. In practice, any of the three will give you excellent SEO if you use their SSR/SSG modes correctly.
Q: Which framework deploys best to Cloudflare?
Remix / React Router v7 has the best Cloudflare Workers/Pages integration — it was designed with edge runtimes in mind and Cloudflare is a first-class deployment target. Astro also has an excellent @astrojs/cloudflare adapter. Next.js supports Cloudflare via the @opennextjs/cloudflare community adapter (not official).
Q: Should I learn Next.js, Remix, or Astro first?
If you're job hunting: Next.js (most jobs by far). If you're building a personal blog or marketing site: Astro (fastest and simplest). If you want to understand web fundamentals (HTTP, forms, progressive enhancement): Remix / React Router v7 (teaches you to think in web standards). All three are worth knowing — they solve different problems well.