Next.js vs Remix vs Astro: Choosing Your Framework Based on What You’re Actually Building
Framework comparison articles usually benchmark rendering speed on a todo app. That’s useless. The Next.js vs Remix vs Astro decision depends on what you’re building, how interactive it needs to be, and what your team already knows. Therefore, this guide evaluates each framework against real application types and gives you a clear recommendation for each.
The Three Philosophies
Next.js says: “Everything is React. Server components render on the server, client components hydrate on the client, and we handle the complexity of deciding what goes where.” It’s the Swiss Army knife — it can build anything, but sometimes you’re carrying tools you don’t need.
Remix says: “The web platform already has great primitives. Use forms, HTTP caching, and progressive enhancement. Build apps that work without JavaScript and get better with it.” It’s opinionated about using web standards, which makes some things beautifully simple and others awkwardly constrained.
Astro says: “Most websites are mostly content. Send HTML with zero JavaScript by default, and only ship JavaScript for the interactive parts.” It’s the content site champion — if your pages are mostly text and images, nothing is faster.
Side-by-Side: The Same Feature in All Three
Let’s build a product page that fetches data from an API. This is the most common page type in web development, and how each framework handles it reveals their core philosophy:
// NEXT.JS 15 — Server Component (default)
// app/products/[id]/page.tsx
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(
'https://api.example.com/products/' + params.id,
{ next: { revalidate: 60 } } // Cache for 60 seconds
).then(r => r.json());
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: {product.price}</p>
{/* This part needs interactivity — client component */}
<Suspense fallback={<p>Loading reviews...</p>}>
<ReviewSection productId={params.id} />
</Suspense>
<AddToCartButton product={product} /> {/* Client component */}
</main>
);
}
// Pros: Granular control over server vs client rendering
// Cons: Mental overhead of knowing which component is server vs client// REMIX — Loader + Component
// app/routes/products.$id.tsx
import { json } from "@remix-run/node";
import { useLoaderData, useFetcher } from "@remix-run/react";
export async function loader({ params }) {
const product = await db.product.findUnique({ where: { id: params.id } });
if (!product) throw new Response("Not Found", { status: 404 });
return json(product, {
headers: { "Cache-Control": "public, max-age=60" }
});
}
export async function action({ request, params }) {
const formData = await request.formData();
await addToCart(params.id, Number(formData.get("quantity")));
return json({ success: true });
}
export default function ProductPage() {
const product = useLoaderData();
const fetcher = useFetcher();
return (
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Progressive enhancement: works WITHOUT JavaScript! */}
<fetcher.Form method="post">
<input type="hidden" name="quantity" value="1" />
<button type="submit">
{fetcher.state === "submitting" ? "Adding..." : "Add to Cart"}
</button>
</fetcher.Form>
</main>
);
}
// Pros: Forms work without JS, HTTP caching is straightforward
// Cons: No server components — everything hydrates on client---
// ASTRO — Frontmatter + Template
// src/pages/products/[id].astro
const { id } = Astro.params;
const product = await fetch('https://api.example.com/products/' + id)
.then(r => r.json());
---
<html>
<head><title>{product.name}</title></head>
<body>
<main>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: {product.price}</p>
<!-- Island: Only THIS component ships JavaScript -->
<AddToCart client:visible productId={id} price={product.price} />
<!-- Reviews load when visible — lazy island -->
<ReviewSection client:visible productId={id} />
</main>
</body>
</html>
<!-- Pros: Zero JS by default, islands only where needed, ANY framework for islands -->
<!-- Cons: Not a full app framework — routing/state management is basic -->Next.js vs Remix vs Astro: When to Choose Each
Choose Next.js when building:
- Full-stack SaaS applications with complex data requirements
- Apps that mix static content, dynamic content, and real-time features
- Projects where your team already knows React and wants a batteries-included framework
- Apps deploying on Vercel (best-in-class DX on their platform)
However, Next.js has real downsides: the App Router’s mental model (server vs client components) is confusing even for experienced React developers, caching behavior is complex and often surprising, and the framework is tightly coupled to Vercel for the best experience.
Choose Remix when building:
- Form-heavy applications (admin panels, dashboards, data entry)
- Apps where progressive enhancement matters (accessibility, low-bandwidth users)
- Applications where you want to leverage HTTP caching and web standards directly
- Teams that find Next.js’s complexity overwhelming and want simpler mental models
Remix’s downside: it ships more JavaScript to the client than Astro (every component hydrates), and it doesn’t have Next.js’s static site generation capabilities for content-heavy sites.
Choose Astro when building:
- Content sites: blogs, documentation, marketing pages, portfolios
- Sites where page load speed is critical (SEO, e-commerce product pages)
- Projects where you want to mix React, Vue, and Svelte components
- Any site where most pages are primarily content with small interactive sections
Astro’s downside: it’s not designed for highly interactive applications. A collaborative editor, real-time dashboard, or complex single-page application would be fighting against Astro’s content-first architecture.
Performance Numbers That Actually Matter
Time to First Byte (TTFB): Astro SSG: ~50ms, Remix with HTTP cache: ~80ms, Next.js with ISR: ~100ms. All are fast enough that users can’t tell the difference.
Total JavaScript shipped (for a blog post page): Astro: 0-5KB (only interactive islands), Remix: ~80-120KB (React + router + hydration), Next.js: ~90-150KB (React + RSC runtime + router). For content pages, Astro’s advantage is dramatic — 20-30x less JavaScript.
For a complex dashboard page with charts, filters, and real-time updates: All three ship roughly the same amount of JavaScript because the interactivity is the bottleneck, not the framework. Choose based on DX, not performance, for highly interactive pages.
Related Reading:
Resources:
In conclusion, Next.js vs Remix vs Astro isn’t about which is “best” — it’s about matching the framework’s philosophy to your application’s reality. Content-heavy? Astro. Form-heavy? Remix. Everything-heavy? Next.js. The worst decision is choosing a framework because it’s popular rather than because it fits.