When selecting a framework for your web application, it is critical to consider the developer experience that it provides. Astro, Remix, and Next.js all build on top of React to provide a more streamlined experience. They have a low learning curve, so if you're already familiar with React, you can quickly pick them up and get started.
We evaluate each to help you decide which framework is appropriate for you, not to determine which is faster or better.
What is Astro?
Astro is a modern web framework built on React and a static site builder that requires little or no JavaScript to deliver lightning-fast, high-performance websites with a modern developer experience. It allows you to create websites using UI components from your favorite JavaScript UI frameworks such as React, Svelte, Vue, and others.
1// Example of an Astro component
2---
3// Component imports and JavaScript execution
4import { Image } from 'astro:assets';
5import ReusableButton from '../components/Button.astro';
6
7// Data fetching in the component
8const response = await fetch('https://api.example.com/data');
9const data = await response.json();
10---
11
12<html lang="en">
13 <head>
14 <title>My Astro Site</title>
15 </head>
16 <body>
17 <h1>Welcome to Astro!</h1>
18 <p>This is a static site with minimal JavaScript</p>
19
20 <!-- Using imported components -->
21 <ReusableButton text="Click me" />
22
23 <!-- Displaying fetched data -->
24 <ul>
25 {data.items.map(item => (
26 <li>{item.name}</li>
27 ))}
28 </ul>
29
30 <!-- Only this component will ship with JavaScript -->
31 <InteractiveCounter client:load initialCount={5} />
32 </body>
33</html>
Astro websites are primarily static, with no JavaScript code by default. When a component (for example, image carousels, dark and light mode) requires JavaScript code to run, Astro only loads that component and any necessary dependencies. The rest of the site remains static lightweight HTML. Check out Astro's Getting Started tutorial for an excellent introduction.
- Astro is more flexible: you can build UI with any popular component library (React, Preact, Vue, Svelte, Solid, and others) or Astro's HTML-like component syntax, similar to HTML + JSX.
- Astro can build statically via SSG or deploy to SSR environments via adapters like Deno, Vercel serverless, Netlify serverless, and Node.js, with more to come.
1// Path: astro.config.mjs - Configuring SSR with an adapter
2
3import { defineConfig } from 'astro/config';
4import vercel from '@astrojs/vercel/serverless';
5import react from '@astrojs/react';
6import vue from '@astrojs/vue';
7
8// https://astro.build/config
9export default defineConfig({
10 // Enable SSR for dynamic rendering
11 output: 'server',
12 // Deploy to Vercel serverless functions
13 adapter: vercel(),
14 // Add support for multiple UI frameworks
15 integrations: [react(), vue()]
16});
What is Next.js?
Next.js is an open-source React framework for quickly creating server-rendered React applications. It adds structure and features and handles the React tooling and configuration required for your application.
1// Path: pages/index.js - Next.js page component
2
3import Head from 'next/head';
4import Link from 'next/link';
5import { useState } from 'react';
6
7export default function Home({ featuredPosts }) {
8 const [count, setCount] = useState(0);
9
10 return (
11 <div>
12 <Head>
13 <title>My Next.js Website</title>
14 </Head>
15
16 <main>
17 <h1>Welcome to my website</h1>
18 <button onClick={() => setCount(count + 1)}>
19 Count: {count}
20 </button>
21
22 <h2>Featured Posts</h2>
23 <ul>
24 {featuredPosts.map(post => (
25 <li key={post.id}>
26 <Link href={`/posts/${post.slug}`}>
27 {post.title}
28 </Link>
29 </li>
30 ))}
31 </ul>
32 </main>
33 </div>
34 );
35}
36
37// Data fetching at build time
38export async function getStaticProps() {
39 const res = await fetch('https://api.example.com/featured-posts');
40 const featuredPosts = await res.json();
41
42 return {
43 props: { featuredPosts },
44 // Re-generate page at most once per hour
45 revalidate: 3600
46 };
47}
It can be used to solve common application requirements like routing, data retrieval, and integrations. Next.js was created to provide an easy-to-use development framework that would reduce the time and effort required to develop full-fledged, SSR-friendly web applications while improving the end user and developer experience. The documentation is a great place to start if you want to begin with this framework.
Next.js fully embraces React Server Components (RSCs), enabling server-side data fetching directly within components. This reduces client-side JavaScript and improves performance. Also, Turbopack is now stable, offering lightning-fast bundling and significantly reducing build times for large projects.
1// Path: app/dashboard/page.js - React Server Component in Next.js
2
3// Server Component (no "use client" directive)
4import { getUser } from '@/lib/auth';
5import { getDashboardData } from '@/lib/data';
6import DashboardMetrics from '@/components/DashboardMetrics';
7import ClientSideInteractions from '@/components/ClientSideInteractions';
8
9export default async function DashboardPage() {
10 // This code runs only on the server
11 const user = await getUser();
12 const { metrics, recentActivity } = await getDashboardData(user.id);
13
14 return (
15 <div>
16 <h1>Welcome back, {user.name}</h1>
17
18 {/* Server component with no client JS */}
19 <DashboardMetrics data={metrics} />
20
21 {/* This component will be hydrated on the client */}
22 <ClientSideInteractions initialData={recentActivity} />
23 </div>
24 );
25}
What is Remix?
Remix is an edge-native, full-stack JavaScript framework for building modern, fast, and resilient user experiences. It unifies the client and server with web standards so you can think less about code and more about your product. Remix is now React Router 7, and it continues to evolve with the latest web development trends.
1// Path: app/routes/posts.$slug.jsx - A Remix route component
2
3import { json } from '@remix-run/node';
4import { useLoaderData, Form, useActionData } from '@remix-run/react';
5import { getPost, addComment } from '~/models/post.server';
6
7// Server-side data loading
8export async function loader({ params }) {
9 const post = await getPost(params.slug);
10
11 if (!post) {
12 throw new Response("Not Found", { status: 404 });
13 }
14
15 return json({ post });
16}
17
18// Server-side form handling
19export async function action({ request, params }) {
20 const formData = await request.formData();
21 const comment = formData.get('comment');
22
23 if (!comment || comment.length < 3) {
24 return json({ error: "Comment must be at least 3 characters" });
25 }
26
27 await addComment({ postSlug: params.slug, text: comment });
28 return json({ success: true });
29}
30
31// Client component using server data
32export default function Post() {
33 const { post } = useLoaderData();
34 const actionData = useActionData();
35
36 return (
37 <article>
38 <h1>{post.title}</h1>
39 <div dangerouslySetInnerHTML={{ __html: post.content }} />
40
41 <h2>Leave a comment</h2>
42 <Form method="post">
43 <textarea name="comment" required minLength={3} />
44 {actionData?.error && <p className="error">{actionData.error}</p>}
45 <button type="submit">Submit</button>
46 </Form>
47
48 <h2>Comments</h2>
49 <ul>
50 {post.comments.map(comment => (
51 <li key={comment.id}>{comment.text}</li>
52 ))}
53 </ul>
54 </article>
55 );
56}
Remix now supports Static Site Generation (SSG), allowing developers to pre-render pages at build time. This feature bridges the gap between Remix and frameworks like Next.js. Currently, Remix's loaders and actions have been optimized for parallel data fetching, reducing load times for complex applications.
1// Example of Static Site Generation in Remix (React Router 7)
2
3// Path: routes/blog.static.$slug.tsx
4
5import { json } from '@remix-run/node';
6import { useLoaderData } from '@remix-run/react';
7import { getPost, getAllPostSlugs } from '~/models/post.server';
8
9// Generate static paths at build time
10export async function generateStaticParams() {
11 const slugs = await getAllPostSlugs();
12 return slugs.map(slug => ({ slug }));
13}
14
15// Load data at build time
16export async function loader({ params }) {
17 const post = await getPost(params.slug);
18 if (!post) {
19 throw new Response("Not Found", { status: 404 });
20 }
21 return json({ post });
22}
23
24export default function BlogPost() {
25 const { post } = useLoaderData();
26 return (
27 <article>
28 <h1>{post.title}</h1>
29 <div dangerouslySetInnerHTML={{ __html: post.content }} />
30 </article>
31 );
32}
Key Features
Let's look at the key features of Astro, Remix, and Next.js to understand what makes each framework unique and how they cater to different development needs.
Astro
- Zero-config: Any config explained will be handled by our
astro add
CLI command (i.e.,add Svelte
support withastro add svelte
).
# Example of using the Astro CLI to add integrations
npm create astro@latest my-project
cd my-project
# Add Tailwind CSS support
npx astro add tailwind
# Add React support
npx astro add react
# Add image optimization
npx astro add image
- Astro is UI-agnostic: meaning you can Bring Your Own UI Framework (BYOF).
1---
2// Path: src/pages/mixed-frameworks.astro
3
4// Using multiple frameworks in the same page
5import ReactCounter from '../components/ReactCounter.jsx';
6import VueToggle from '../components/VueToggle.vue';
7import SvelteCard from '../components/SvelteCard.svelte';
8---
9
10<html>
11 <head>
12 <title>Multi-Framework Page</title>
13 </head>
14 <body>
15 <h1>Using Multiple Frameworks</h1>
16
17 <!-- React component -->
18 <ReactCounter client:visible initialCount={0} />
19
20 <!-- Vue component -->
21 <VueToggle client:idle />
22
23 <!-- Svelte component -->
24 <SvelteCard client:only="svelte" title="Svelte Card" />
25 </body>
26</html>
- Easy to use: Astro's goal is to be accessible to every web developer. Astro was designed to feel familiar and approachable regardless of skill level or experience with web development.
- Fast by default: An Astro website can load 40% faster with 90% less JavaScript than the site built with the most popular React web framework.
- Server-first: Astro leverages server-side rendering over client-side rendering as much as possible.
- Content-based: Astro's unique focus on content lets Astro make tradeoffs and deliver unmatched performance features that wouldn't make sense for more application-focused web frameworks to implement.
- Fully-featured but flexible: Astro is an all-in-one web framework with everything you need to build a website. Astro includes a component syntax, file-based routing, asset handling, a build process, bundling, optimizations, data fetching, and more. You can build great websites without ever reaching outside of Astro's core feature set.
- Server Islands: Astro now supports server-rendered components within static pages, enabling hybrid rendering for dynamic interactivity without sacrificing performance.
1---
2// Path: src/pages/islands-example.astro
3
4import StaticHeader from '../components/StaticHeader.astro';
5import DynamicCounter from '../components/DynamicCounter.jsx';
6import HeavyDataComponent from '../components/HeavyDataComponent.jsx';
7---
8
9<html>
10 <head>
11 <title>Islands Architecture Example</title>
12 </head>
13 <body>
14 <!-- Static, no JavaScript -->
15 <StaticHeader title="Islands Architecture" />
16
17 <!-- Interactive, hydrated immediately -->
18 <DynamicCounter client:load />
19
20 <!-- Interactive, hydrated only when visible -->
21 <HeavyDataComponent client:visible />
22
23 <!-- Static content continues... -->
24 <footer>
25 <p>ยฉ 2025 My Website</p>
26 </footer>
27 </body>
28</html>
- Content Layer API: Expanded Content Collections allow seamless integration with external APIs and databases, making Astro more versatile for dynamic content.
1// Path: src/content/config.ts
2
3import { defineCollection, z } from 'astro:content';
4
5// Define a schema for blog posts
6const blogCollection = defineCollection({
7 type: 'content',
8 schema: z.object({
9 title: z.string(),
10 publishDate: z.date(),
11 author: z.string(),
12 featured: z.boolean().default(false),
13 tags: z.array(z.string()).default([])
14 })
15});
16
17// Export collections
18export const collections = {
19 'blog': blogCollection
20};
21
22// src/pages/blog/[...slug].astro
23---
24import { getCollection, getEntry } from 'astro:content';
25
26// Generate paths at build time
27export async function getStaticPaths() {
28 const blogEntries = await getCollection('blog');
29 return blogEntries.map(entry => ({
30 params: { slug: entry.slug },
31 props: { entry }
32 }));
33}
34
35const { entry } = Astro.props;
36const { Content } = await entry.render();
37---
38
39<article>
40 <h1>{entry.data.title}</h1>
41 <p>By {entry.data.author} on {entry.data.publishDate.toLocaleDateString()}</p>
42 <Content />
43</article>
- Astro DB: Built-in database management powered by Drizzle, enabling full-stack capabilities for more complex applications.
1// Path: db/schema.ts
2
3import { defineDb, defineTable, column } from 'astro:db';
4
5export const Users = defineTable({
6 columns: {
7 id: column.number({ primaryKey: true }),
8 name: column.text(),
9 email: column.text({ unique: true }),
10 created_at: column.date()
11 }
12});
13
14export default defineDb({
15 tables: { Users }
16});
17
18// src/pages/api/users.ts
19import { db } from 'astro:db';
20import { Users } from '../../db/schema';
21
22export async function GET() {
23 const users = await db.select().from(Users);
24 return new Response(JSON.stringify(users), {
25 headers: { 'Content-Type': 'application/json' }
26 });
27}
28
29export async function POST({ request }) {
30 const { name, email } = await request.json();
31
32 const user = await db.insert(Users).values({
33 name,
34 email,
35 created_at: new Date()
36 }).returning();
37
38 return new Response(JSON.stringify(user), {
39 status: 201,
40 headers: { 'Content-Type': 'application/json' }
41 });
42}
Remix
- Routes: Like other frameworks, Remix allows developers to manage the different routes of their web projects using JavaScript/TypeScript files that contain handler functions. We can generate routes on our website to create files that follow the file system hierarchy of our projects, creating analog URLs for our pages. Remix routes work using the partial routing feature provided by React-Router.
1// File-based routing in Remix
2
3// Path: app/routes/_layout.tsx - Parent layout
4
5import { Outlet, Link } from '@remix-run/react';
6
7export default function Layout() {
8 return (
9 <div className="app-container">
10 <header>
11 <nav>
12 <Link to="/">Home</Link>
13 <Link to="/about">About</Link>
14 <Link to="/blog">Blog</Link>
15 </nav>
16 </header>
17
18 <main>
19 <Outlet /> {/* Child routes render here */}
20 </main>
21
22 <footer>ยฉ 2025 My Remix App</footer>
23 </div>
24 );
25}
26
27// app/routes/_layout.about.tsx - About page
28export default function About() {
29 return (
30 <div>
31 <h1>About Us</h1>
32 <p>Learn more about our company...</p>
33 </div>
34 );
35}
36
37// app/routes/_layout.blog.tsx - Blog index
38export default function BlogIndex() {
39 return (
40 <div>
41 <h1>Blog</h1>
42 <p>Read our latest articles</p>
43 </div>
44 );
45}
46
47// app/routes/_layout.blog.$slug.tsx - Dynamic blog post route
48import { useParams } from '@remix-run/react';
49
50export default function BlogPost() {
51 const { slug } = useParams();
52 return <h1>Blog Post: {slug}</h1>;
53}
- Nested components: Remix allows you to manage nested pages and components. We can create a file to handle a certain route and, at the same level in the file system, a folder with the same name. All the files we create inside that folder will be nested components of the parent route instead of different pages.
- Error Handling: Nested components bring another benefit: if an error occurs while rendering a certain component, it doesn't affect the other nested parts of the page.
1// Path: app/routes/dashboard.tsx - Parent dashboard route
2
3import { Outlet } from '@remix-run/react';
4
5export default function Dashboard() {
6 return (
7 <div className="dashboard">
8 <h1>Dashboard</h1>
9 <nav>
10 <Link to="/dashboard/stats">Stats</Link>
11 <Link to="/dashboard/settings">Settings</Link>
12 </nav>
13 <div className="dashboard-content">
14 <Outlet />
15 </div>
16 </div>
17 );
18}
19
20// app/routes/dashboard.stats.tsx - Stats page with error boundary
21import { useLoaderData } from '@remix-run/react';
22
23export async function loader() {
24 // Potentially could fail
25 const stats = await fetchUserStats();
26 return { stats };
27}
28
29// This only affects this route, not parent or siblings
30export function ErrorBoundary() {
31 return (
32 <div className="error-container">
33 <h2>Error loading stats</h2>
34 <p>There was a problem loading your statistics.</p>
35 <button>Try again</button>
36 </div>
37 );
38}
39
40export default function Stats() {
41 const { stats } = useLoaderData();
42 return (
43 <div>
44 <h2>Your Statistics</h2>
45 <div className="stats-grid">
46 {Object.entries(stats).map(([key, value]) => (
47 <div key={key} className="stat-card">
48 <h3>{key}</h3>
49 <p>{value}</p>
50 </div>
51 ))}
52 </div>
53 </div>
54 );
55}
- Forms: As Remix focuses on web standards, it handles forms using native methods (POST, PUT, DELETE, PATCH) instead of JavaScript.
1// app/routes/products.$id.tsx - Edit product form
2import { Form, useLoaderData, useActionData, redirect } from '@remix-run/react';
3import { json } from '@remix-run/node';
4import { getProduct, updateProduct } from '~/models/product.server';
5
6export async function loader({ params }) {
7 const product = await getProduct(params.id);
8 if (!product) {
9 throw new Response("Product not found", { status: 404 });
10 }
11 return json({ product });
12}
13
14export async function action({ request, params }) {
15 const formData = await request.formData();
16 const name = formData.get('name');
17 const price = parseFloat(formData.get('price'));
18
19 const errors = {};
20 if (!name) errors.name = "Name is required";
21 if (isNaN(price) || price <= 0) errors.price = "Price must be a positive number";
22
23 if (Object.keys(errors).length > 0) {
24 return json({ errors });
25 }
26
27 await updateProduct(params.id, { name, price });
28 return redirect('/products');
29}
30
31export default function EditProduct() {
32 const { product } = useLoaderData();
33 const actionData = useActionData();
34
35 return (
36 <div>
37 <h1>Edit Product: {product.name}</h1>
38
39 <Form method="post">
40 <div>
41 <label>
42 Name:
43 <input name="name" defaultValue={product.name} />
44 </label>
45 {actionData?.errors?.name && <p className="error">{actionData.errors.name}</p>}
46 </div>
47
48 <div>
49 <label>
50 Price:
51 <input name="price" type="number" step="0.01" defaultValue={product.price} />
52 </label>
53 {actionData?.errors?.price && <p className="error">{actionData.errors.price}</p>}
54 </div>
55
56 <div className="actions">
57 <button type="submit">Save Changes</button>
58 </div>
59 </Form>
60 </div>
61 );
62}
- Loaders and Actions: Remix provides two different types of functions to create server-side dynamic content. The loader functions handle GET HTTP requests in the server, mainly used to get data from different sources.
1// Path: app/routes/products.tsx - Loader and Action example
2
3import { json, redirect } from '@remix-run/node';
4import { useLoaderData, Form } from '@remix-run/react';
5import { getProducts, createProduct } from '~/models/products.server';
6
7// Loader for GET requests
8export async function loader() {
9 const products = await getProducts();
10 return json({ products });
11}
12
13// Action for POST/PUT/DELETE requests
14export async function action({ request }) {
15 const formData = await request.formData();
16 const intent = formData.get('intent');
17
18 if (intent === 'create') {
19 const name = formData.get('name');
20 const price = parseFloat(formData.get('price'));
21
22 await createProduct({ name, price });
23 return redirect('/products');
24 }
25
26 return json({ error: 'Invalid intent' });
27}
28
29export default function Products() {
30 const { products } = useLoaderData();
31
32 return (
33 <div>
34 <h1>Products</h1>
35
36 <h2>Add New Product</h2>
37 <Form method="post">
38 <input type="hidden" name="intent" value="create" />
39 <input name="name" placeholder="Product name" required />
40 <input name="price" type="number" step="0.01" placeholder="Price" required />
41 <button type="submit">Add Product</button>
42 </Form>
43
44 <h2>Product List</h2>
45 <ul>
46 {products.map(product => (
47 <li key={product.id}>
48 {product.name} - ${product.price.toFixed(2)}
49 </li>
50 ))}
51 </ul>
52 </div>
53 );
54}
- Static Site Generation (SSG): Remix now supports SSG, allowing developers to pre-render pages at build time. This feature bridges the gap between Remix and frameworks like Next.js.
- Enhanced Data Fetching: Optimized loaders and actions for parallel data fetching, reducing load times for complex applications.
1// Parallel data fetching in Remix
2export async function loader() {
3 // Fetch multiple data sources in parallel
4 const [products, categories, featuredItems] = await Promise.all([
5 getProducts(),
6 getCategories(),
7 getFeaturedItems()
8 ]);
9
10 return json({
11 products,
12 categories,
13 featuredItems
14 });
15}
Next.js
- Async Components & Data Fetching: Async components are a new technique for obtaining data for server-rendered components introduced in Next.js 13. We can render async components using Promises with async and await.
1// Path: app/dashboard/page.js - Async Server Component
2
3async function fetchDashboardData() {
4 const res = await fetch('https://api.example.com/dashboard', {
5 next: { revalidate: 60 } // Revalidate every 60 seconds
6 });
7
8 if (!res.ok) {
9 throw new Error('Failed to fetch dashboard data');
10 }
11
12 return res.json();
13}
14
15export default async function DashboardPage() {
16 // Data is fetched on the server during rendering
17 const data = await fetchDashboardData();
18
19 return (
20 <div className="dashboard">
21 <h1>Dashboard</h1>
22 <div className="stats-grid">
23 <div className="stat-card">
24 <h2>Total Users</h2>
25 <p className="stat-value">{data.totalUsers.toLocaleString()}</p>
26 </div>
27 <div className="stat-card">
28 <h2>Active Subscriptions</h2>
29 <p className="stat-value">{data.activeSubscriptions.toLocaleString()}</p>
30 </div>
31 <div className="stat-card">
32 <h2>Revenue</h2>
33 <p className="stat-value">${data.revenue.toLocaleString()}</p>
34 </div>
35 </div>
36 </div>
37 );
38}
- React Server Components: Server components enable us to execute and render React components on the server side, resulting in faster delivery, a smaller JavaScript bundle, and lower client-side rendering costs.
1// Path: app/products/[id]/page.js - Server Component
2
3import { notFound } from 'next/navigation';
4import AddToCartButton from '@/components/AddToCartButton'; // Client Component
5import ProductReviews from '@/components/ProductReviews'; // Server Component
6
7async function getProduct(id) {
8 const res = await fetch(`https://api.example.com/products/${id}`);
9 if (!res.ok) return null;
10 return res.json();
11}
12
13export default async function ProductPage({ params }) {
14 const product = await getProduct(params.id);
15
16 if (!product) {
17 notFound();
18 }
19
20 return (
21 <div className="product-page">
22 <h1>{product.name}</h1>
23 <p className="price">${product.price.toFixed(2)}</p>
24 <div className="description">{product.description}</div>
25
26 {/* Client Component - will be hydrated on the client */}
27 <AddToCartButton productId={product.id} />
28
29 {/* Server Component - no client JS needed */}
30 <ProductReviews productId={product.id} />
31 </div>
32 );
33}
34
35// components/AddToCartButton.js - Client Component
36'use client';
37
38import { useState } from 'react';
39
40export default function AddToCartButton({ productId }) {
41 const [isAdding, setIsAdding] = useState(false);
42
43 async function handleAddToCart() {
44 setIsAdding(true);
45 await addToCart(productId);
46 setIsAdding(false);
47 }
48
49 return (
50 <button
51 onClick={handleAddToCart}
52 disabled={isAdding}
53 className="add-to-cart-button"
54 >
55 {isAdding ? 'Adding...' : 'Add to Cart'}
56 </button>
57 );
58}
app
/ Directory for File-Based Routing: Routes are defined using the structure of your project directory. By placing an entry point in the app directory, you can create a new route. This feature is now stable and fully supported for production use.
1// Next.js App Router structure example
2// app/page.js - Homepage
3export default function HomePage() {
4 return <h1>Welcome to our website</h1>;
5}
6
7// app/about/page.js - About page (/about)
8export default function AboutPage() {
9 return <h1>About Us</h1>;
10}
11
12// app/blog/[slug]/page.js - Dynamic blog post page (/blog/*)
13export default function BlogPost({ params }) {
14 return <h1>Blog Post: {params.slug}</h1>;
15}
16
17// app/api/products/route.js - API route (/api/products)
18export async function GET(request) {
19 const products = await fetchProducts();
20 return Response.json(products);
21}
- Lightning Fast Bundling: Turbopack, introduced with Next.js 13, is now stable and offers lightning-fast bundling, significantly reducing build times for large projects.
1// Path: next.config.js - Enabling Turbopack
2
3/** @type {import('next').NextConfig} */
4const nextConfig = {
5 // Enable Turbopack for development
6 experimental: {
7 turbo: true,
8 }
9};
10
11module.exports = nextConfig;
12
13// Using with npm scripts in package.json
14{
15 "scripts": {
16 "dev": "next dev --turbo",
17 "build": "next build",
18 "start": "next start"
19 }
20}
- Built-in CSS and Sass support: Support for any CSS-in-JS library.
1// Using CSS Modules in Next.js
2
3// Path: components/Button.module.css
4
5.button {
6 background: blue;
7 color: white;
8 padding: 8px 16px;
9 border-radius: 4px;
10 border: none;
11 cursor: pointer;
12}
13
14.button:hover {
15 background: darkblue;
16}
17
18// components/Button.js
19import styles from './Button.module.css';
20
21export default function Button({ children, onClick }) {
22 return (
23 <button className={styles.button} onClick={onClick}>
24 {children}
25 </button>
26 );
27}
28
29// Global CSS in app/globals.css
30body {
31 font-family: system-ui, sans-serif;
32 margin: 0;
33 padding: 0;
34}
35
36// Using in app/layout.js
37import './globals.css';
38
39export default function RootLayout({ children }) {
40 return (
41 <html lang="en">
42 <body>{children}</body>
43 </html>
44 );
45}
- Static Exports: Next.js allows you to export a fully static site from your app using the
next export
command.
1// Path: next.config.js - Static export configuration
2
3/** @type {import('next').NextConfig} */
4const nextConfig = {
5 output: 'export',
6 // Optional: Change the output directory
7 distDir: 'out',
8 // Optional: Add basePath for deployment to a subdirectory
9 basePath: '/docs'
10};
11
12module.exports = nextConfig;
13
14// package.json
15{
16 "scripts": {
17 "build": "next build",
18 "export": "next export" // For older Next.js versions
19 }
20}
- Advanced Server Actions: Next.js 14 introduces server actions for handling data mutations, simplifying backend logic without requiring separate API routes.
1// Path: app/actions.js - Server Actions
2
3'use server';
4
5import { cookies } from 'next/headers';
6import { revalidatePath } from 'next/cache';
7import { db } from '@/lib/db';
8
9export async function addToCart(formData) {
10 const productId = formData.get('productId');
11 const quantity = parseInt(formData.get('quantity') || '1');
12
13 // Get user from cookie
14 const userId = cookies().get('userId')?.value;
15 if (!userId) {
16 throw new Error('User not authenticated');
17 }
18
19 try {
20 await db.cart.upsert({
21 where: {
22 userId_productId: {
23 userId,
24 productId
25 }
26 },
27 update: {
28 quantity: { increment: quantity }
29 },
30 create: {
31 userId,
32 productId,
33 quantity
34 }
35 });
36
37 // Revalidate cart page
38 revalidatePath('/cart');
39
40 return { success: true };
41 } catch (error) {
42 return { success: false, error: error.message };
43 }
44}
45
46// app/products/[id]/page.js
47import { addToCart } from '@/app/actions';
48
49export default function ProductPage({ params }) {
50 // ...product details
51
52 return (
53 <div>
54 {/* ... */}
55 <form action={addToCart}>
56 <input type="hidden" name="productId" value={params.id} />
57 <input type="number" name="quantity" min="1" defaultValue="1" />
58 <button type="submit">Add to Cart</button>
59 </form>
60 </div>
61 );
62}
- Enhanced Caching: Improved caching mechanisms, including stale-while-revalidate (SWR), ensure faster content delivery and better user experiences.
1// Using SWR for client-side data fetching
2'use client';
3
4import useSWR from 'swr';
5
6// Reusable fetcher function
7const fetcher = (...args) => fetch(...args).then(res => res.json());
8
9export default function ProductList() {
10 const { data, error, isLoading } = useSWR('/api/products', fetcher, {
11 // Revalidate every 10 seconds
12 refreshInterval: 10000,
13 // Keep data even when fetching fails
14 revalidateOnError: false
15 });
16
17 if (isLoading) return <div>Loading products...</div>;
18 if (error) return <div>Error loading products</div>;
19
20 return (
21 <div>
22 <h1>Products</h1>
23 <ul>
24 {data.map(product => (
25 <li key={product.id}>{product.name}</li>
26 ))}
27 </ul>
28 </div>
29 );
30}
Hydration
Hydration is a client-side JavaScript technique for converting a static HTML page into a dynamic page. This provides a pleasant user experience by displaying a rendered component on the page but with attached event handlers. Hydration occurs before user interaction on static pages. The user experience suffers as a result.
Astro
Astro handles hydration through a method known as partial hydration. This approach loads individual components only when needed, leaving the rest of the page as static HTML. The island architecture is central to this process, as it ensures that only the necessary JavaScript is loaded, improving performance.
1---
2// Path: src/pages/hydration-example.astro
3
4import StaticComponent from '../components/StaticComponent.astro';
5import InteractiveCounter from '../components/InteractiveCounter.jsx';
6import LazyLoadedComponent from '../components/LazyLoadedComponent.jsx';
7import HeavyComponent from '../components/HeavyComponent.jsx';
8---
9
10<html>
11 <head><title>Hydration Example</title></head>
12 <body>
13 <!-- Static component, no JavaScript -->
14 <StaticComponent />
15
16 <!-- Hydration strategies -->
17 <!-- 1. Hydrate immediately on page load -->
18 <InteractiveCounter client:load />
19
20 <!-- 2. Hydrate when component is visible in viewport -->
21 <LazyLoadedComponent client:visible />
22
23 <!-- 3. Hydrate after page load, when browser is idle -->
24 <HeavyComponent client:idle />
25
26 <!-- 4. Hydrate only when media query matches -->
27 <div client:media="(max-width: 768px)">
28 This only hydrates on mobile devices
29 </div>
30
31 <!-- 5. Don't hydrate at all on the server, render only on client -->
32 <div client:only="react">
33 This renders only on the client
34 </div>
35 </body>
36</html>
- In most cases, Astro websites load significantly faster than Next.js websites because Astro automatically strips unnecessary JavaScript and hydrates only the components that require interactivity.
- By default, Astro delivers static HTML with minimal JavaScript, resulting in faster page loads and better performance for content-heavy sites.
Next.js
Next.js has evolved its hydration strategy with the introduction of React Server Components (RSCs) and Selective Hydration in Next.js 14. While it traditionally required full-page hydration, it now supports granular hydration for interactive components, reducing the amount of JavaScript sent to the client.
1// Path: app/page.js - Server Component
2
3// This doesn't require client-side JS
4export default function HomePage() {
5 return (
6 <div>
7 <h1>Welcome to our site</h1>
8 <p>This server component requires no client-side JavaScript</p>
9
10 {/* Client components are selectively hydrated */}
11 <ClientSideInteractive />
12 </div>
13 );
14}
15
16// components/ClientSideInteractive.js
17'use client'; // Mark as client component
18
19import { useState } from 'react';
20
21export default function ClientSideInteractive() {
22 // Client-side state and interactivity
23 const [count, setCount] = useState(0);
24
25 return (
26 <div>
27 <p>This component is hydrated with JavaScript</p>
28 <p>Count: {count}</p>
29 <button onClick={() => setCount(count + 1)}>
30 Increment
31 </button>
32 </div>
33 );
34}
- Next.js allows developers to hydrate only the interactive parts of a page, improving performance for static-heavy content.
- Next.js continues to support zero-JavaScript pages through Static Site Generation (SSG) and Incremental Static Regeneration (ISR).
- Unlike Astro's island architecture, which hydrates individual components by default, Next.js still prioritizes full-page hydration but offers selective hydration as an experimental feature for optimization.
Remix
Remix does not support partial hydration. There are assumptions that Remix will function with the new React 19 suspense features, but Remix does not allow partial hydration.
1// Path: app/entry.client.jsx - Client-side hydration in Remix
2
3import { RemixBrowser } from '@remix-run/react';
4import { startTransition, StrictMode } from 'react';
5import { hydrateRoot } from 'react-dom/client';
6
7// The entire app is hydrated at once
8startTransition(() => {
9 hydrateRoot(
10 document,
11 <StrictMode>
12 <RemixBrowser />
13 </StrictMode>
14 );
15});
Loading Speed
The loading speed plays an important role in web application, because it directly affects user experience, SEO performance and system performance. The loading speed optimization techniques of Astro, Remix and Next.js depend on their specific architectural methods and built-in features. Let's take a look at how these frameworks handle performance and what makes them fast.
Astro
Astro is fast, basically designed for speed. The island architecture strategy aids in SEO because it ranks highly on on-site search engines. It offers a fantastic user experience and has less boilerplate code. It supports most CSS libraries and frameworks and provides a great base for style support.
1// Path: astro.config.mjs - Performance optimizations
2
3import { defineConfig } from 'astro/config';
4import image from '@astrojs/image';
5import compress from 'astro-compress';
6
7export default defineConfig({
8 // Image optimization
9 integrations: [
10 image({
11 serviceEntryPoint: '@astrojs/image/sharp'
12 }),
13 // Compress HTML, CSS, and JavaScript
14 compress()
15 ],
16
17 // Enable view transitions for smooth navigation
18 experimental: {
19 viewTransitions: true
20 },
21
22 // CSS optimizations
23 vite: {
24 build: {cssCodeSplit: true,
25 cssMinify: true,
26 }
27 }
28});
Remix
Remix claims that data retrieval is sped up by loading data in parallel on the server. Remix can prerender pages on the server because it supports server-side rendering. In contrast to Remix, Astro provides a statically-bundled HTML file with minimal to no JavaScript.
Why the Remix rewrite is fast?
- Instead of caching documents with SSG or SWR, this version caches data at the edge in Redis.
- It runs the application at the edge too with Fly.io.
- Quick image optimization Resource Route that writes to a persistent volume.
- It's its own CDN. This might have been difficult to build a few years ago, but the server landscape has changed significantly in the past few years and is only getting better.
1// Path: remix.config.js - Performance configuration
2
3/** @type {import('@remix-run/dev').AppConfig} */
4module.exports = {
5 // Deploy to edge runtime
6 serverBuildTarget: "vercel",
7 server: process.env.NODE_ENV === "development" ? undefined : "./server.js",
8 ignoredRouteFiles: ["**/.*"],
9 // Optimize JS output
10 future: {
11 v2_routeConvention: true,
12 v2_meta: true,
13 v2_errorBoundary: true,
14 v2_normalizeFormMethod: true,
15 },
16 // Caching strategy
17 serverDependenciesToBundle: "all",
18
19 // Custom Resource Routes for optimized assets
20 routes: function(defineRoutes) {
21 return defineRoutes((route) => {
22 route(
23 "/resources/image/:width/:height/:src",
24 "routes/resources/image.tsx"
25 );
26 });
27 }
28};
29
30// routes/resources/image.tsx - Image optimization route
31import { LoaderFunction } from "@remix-run/node";
32import { optimizeImage } from "~/utils/images.server";
33
34export const loader: LoaderFunction = async ({ params, request }) => {
35 const { width, height, src } = params;
36 const format = new URL(request.url).searchParams.get("format") || "webp";
37
38 try {
39 const { buffer, contentType } = await optimizeImage({
40 src: src as string,
41 width: parseInt(width as string),
42 height: parseInt(height as string),
43 format,
44 });
45
46 return new Response(buffer, {
47 headers: {
48 "Content-Type": contentType,
49 "Cache-Control": "public, max-age=31536000, immutable",
50 },
51 });
52 } catch (error) {
53 return new Response("Image optimization failed", { status: 500 });
54 }
55};
Why the Remix port is fast?
- Remix now supports Static Site Generation (SSG), allowing developers to pre-render pages at build time.
- The result is the same: a static document at the edge (even on the same CDN, Vercel's). The difference is how the documents get there. Instead of fetching all the data and rendering the pages to static documents at build/deploy time, the cache is primed when you get traffic.
- Documents are served from the cache and revalidated in the background for the next visitor.
1// Parallel data fetching for improved performance
2
3// Path: app/routes/dashboard.tsx
4
5import { json } from '@remix-run/node';
6import { useLoaderData } from '@remix-run/react';
7import { getUser, getStats, getActivities, getNotifications } from '~/models/dashboard.server';
8
9export async function loader({ request }) {
10 const userId = await getUserId(request);
11
12 // Fetch all data in parallel for improved performance
13 const [user, stats, activities, notifications] = await Promise.all([
14 getUser(userId),
15 getStats(userId),
16 getActivities(userId),
17 getNotifications(userId)
18 ]);
19
20 return json({
21 user,
22 stats,
23 activities,
24 notifications
25 });
26}
27
28export default function Dashboard() {
29 const { user, stats, activities, notifications } = useLoaderData();
30 // Render dashboard UI with fetched data...
31}
Next.js
Next.js boasts of its server-side rendering and static builds features. Next.js also includes several pre-built techniques for data retrieval.
Why Next.js is fast?
- The homepage uses Static Site Generation (SSG) with
getStaticProps
. - At build time, Next.js pulls data from Shopify, renders a page to an HTML file, and puts it in the public directory.
- When the site is deployed, the static file is served at the edge (out of Vercel's CDN) instead of hitting an origin server at a single location.
- When a request comes in, the CDN serves the file.
- Data loading and rendering have already been done ahead of time, so the visitor doesn't pay the download + render cost.
- The CDN is distributed globally, close to users (this is "the edge"), so requests for statically generated documents don't have to travel to a single origin server.
- Next.js now supports React Server Components (RSCs) and Selective Hydration, reducing the amount of JavaScript sent to the client and improving performance for dynamic content.
1// Path: pages/index.js - Static Site Generation
2
3export default function HomePage({ products, categories }) {
4 return (
5 <div>
6 <h1>Welcome to our store</h1>
7 <div className="product-grid">
8 {products.map(product => (
9 <ProductCard key={product.id} product={product} />
10 ))}
11 </div>
12 </div>
13 );
14}
15
16// This runs at build time in production
17export async function getStaticProps() {
18 // Fetch data from CMS, database, or API
19 const products = await fetchFeaturedProducts();
20 const categories = await fetchCategories();
21
22 return {
23 props: {
24 products,
25 categories
26 },
27 // Re-generate at most once per hour
28 revalidate: 3600
29 };
30}
31
32// Incremental Static Regeneration example
33// pages/products/[id].js
34export default function Product({ product }) {
35 // Render product details...
36}
37
38export async function getStaticPaths() {
39 // Pre-render only the most popular products
40 const popularProducts = await fetchPopularProducts();
41
42 return {
43 paths: popularProducts.map(product => ({
44 params: { id: product.id.toString() }
45 })),
46 // Enable ISR for products not generated at build time
47 fallback: 'blocking'
48 };
49}
50
51export async function getStaticProps({ params }) {
52 const product = await fetchProduct(params.id);
53
54 return {
55 props: { product },
56 revalidate: 60 // Update every minute if requested
57 };
58}
SSR
Server-side rendering (SSR) refers to the process of pre-rendering client-side single-page applications on the server and then sending a fully rendered page on user request. Server-side rendering is essential because server-side rendered applications are SEO-friendly and fast. Apps that support server-side rendering are usually due to their reduced page load time.
Astro, Remix, and Next.js offer server-side rendering (SSR) to generate the markup and content of our pages from the web server before sending it to the client.
1// Astro SSR configuration
2
3// Path: astro.config.mjs
4import { defineConfig } from 'astro/config';
5import nodejs from '@astrojs/node';
6
7export default defineConfig({
8 output: 'server',
9 adapter: nodejs({
10 mode: 'standalone'
11 })
12});
13
14// Next.js SSR example
15// pages/products/[id].js
16export default function Product({ product }) {
17 return (
18 <div>
19 <h1>{product.name}</h1>
20 <p>{product.description}</p>
21 <p>${product.price}</p>
22 </div>
23 );
24}
25
26export async function getServerSideProps({ params, req, res }) {
27 // This runs on every request
28 const product = await fetchProduct(params.id);
29
30 // Set cache headers
31 res.setHeader(
32 'Cache-Control',
33 'public, s-maxage=10, stale-while-revalidate=59'
34 );
35
36 return {
37 props: { product }
38 };
39}
40
41// Remix SSR example
42// app/routes/products.$id.jsx
43import { json } from '@remix-run/node';
44import { useLoaderData } from '@remix-run/react';
45
46export async function loader({ params, request }) {
47 const product = await fetchProduct(params.id);
48
49 return json(
50 { product },
51 {
52 headers: {
53 'Cache-Control': 'max-age=300, s-maxage=3600'
54 }
55 }
56 );
57}
58
59export default function Product() {
60 const { product } = useLoaderData();
61
62 return (
63 <div>
64 <h1>{product.name}</h1>
65 <p>{product.description}</p>
66 <p>${product.price}</p>
67 </div>
68 );
69}
Ease Of Use
Next.js, Astro, and Remix have a short learning curve. Because they are all based on React, you only need a basic understanding of React to set up Next.js, Astro, and Remix. They all feature developer-friendly documentation, making them simple to use and configure.
Next includes the create-next-app
CLI command for quickly launching a Next.js application. For bootstrapping an Astro application, use the create astro@latest
command, whereas Remix uses the create-remix@latest
command for Remix apps.
# Create a new Next.js app
npx create-next-app@latest my-next-app
# Create a new Astro app
npm create astro@latest my-astro-app
# Create a new Remix app
npx create-remix@latest my-remix-app
Conclusion
We looked at Astro, a highly performant library for shipping no JavaScript code, Remix, a framework for handling client-side and server-side code, and Next.js, which includes data fetching methods such as ISR, CSR, SSG, and SSR.
We looked at key features, loading speed, hydration, server-side rendering, and ease of use. This allows you to select the framework to utilize for your projects. It's not about which framework is better but which solves your problem best.
1// Framework selection helper
2function recommendFramework(requirements) {
3 // Content-heavy site with minimal interactivity
4 if (requirements.contentFocused && requirements.minimalInteractivity) {
5 return "Astro";
6 }
7
8 // Full-stack application with forms and data mutations
9 if (requirements.formHandling && requirements.dataManagement) {
10 return "Remix";
11 }
12
13 // Enterprise application with complex requirements
14 if (requirements.enterprise && requirements.ecosystem) {
15 return "Next.js";
16 }
17
18 // General recommendation based on team experience
19 return requirements.teamExperience || "Next.js";
20}
21
22// Example usage
23console.log(recommendFramework({
24 contentFocused: true,
25 minimalInteractivity: true,
26 teamExperience: "React"
27})); // Output: "Astro"
If you would like to start building with these frameworks, check out these resources:
- How to Build a Blog App with Remix and Strapi CMS
- How to Create an SSG (Static Site Generation) Application with Strapi Webhooks and NextJs
Continue discussing this topic further or connect with more people using Strapi on our Discord community. It is a great place to share your thoughts, ask questions, and participate in live discussions.
Technical Writer, Tech Blogger, Developer Advocate Intern, Self-Taught Frontend Developer.
I am Software Engineer and Technical Writer. Proficient Server-side scripting and Database setup. Agile knowledge of Python, NodeJS, ReactJS, and PHP. When am not coding, I share my knowledge and experience with the other developers through technical articles