Both are full-stack React frameworks. Both handle routing, data loading, and server rendering. Both use TypeScript by default. The similarities end there.
Next.js is built by Vercel and optimized for their platform — edge caching, partial prerendering, incremental static regeneration. Remix is built around web standards — native Request/Response, HTML form actions, progressive enhancement. It runs anywhere Node.js runs with no vendor dependency.
Data Loading
Next.js: Server Components Fetch Directly
// app/dashboard/page.tsx — direct DB access, no API layer needed
export default async function Dashboard() {
const [user, stats, recentOrders] = await Promise.all([
db.user.findUnique({ where: { id: getCurrentUserId() } }),
db.order.aggregate({ _count: true }),
db.order.findMany({ take: 10, orderBy: { createdAt: 'desc' } })
])
return (
<div>
<h1>Welcome, {user.name}</h1>
<Stats data={stats} />
<RecentOrders orders={recentOrders} />
</div>
)
}
Next.js deduplicates identical fetches across the component tree automatically.
Remix: Loaders Fetch, Components Consume
// app/routes/dashboard.tsx
export async function loader({ request }: LoaderFunctionArgs) {
const userId = await getUserId(request)
const [user, stats, recentOrders] = await Promise.all([
db.user.findUnique({ where: { id: userId } }),
db.order.aggregate({ _count: true }),
db.order.findMany({ take: 10, orderBy: { createdAt: 'desc' } })
])
return json({ user, stats, recentOrders })
}
export default function Dashboard() {
const { user, stats, recentOrders } = useLoaderData<typeof loader>()
return (/* same JSX */)
}
Explicit separation: loader is testable in isolation without rendering any UI.
Forms and Mutations
This is Remix's strongest differentiator.
Next.js: Server Actions (requires JavaScript)
'use server'
export async function createPost(formData: FormData) {
const parsed = createPostSchema.safeParse(Object.fromEntries(formData))
if (!parsed.success) return { error: parsed.error.flatten().fieldErrors }
await db.post.create({ data: parsed.data })
revalidatePath('/blog')
}
// Client component needed for pending state
'use client'
const [state, action, isPending] = useActionState(createPost, null)
return <form action={action}><button disabled={isPending}>Save</button></form>
Remix: Native Form Actions (progressive enhancement)
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData()
const parsed = createPostSchema.safeParse(Object.fromEntries(formData))
if (!parsed.success) return json({ errors: parsed.error.flatten().fieldErrors }, { status: 400 })
await db.post.create({ data: parsed.data })
return redirect('/blog')
}
export default function NewPost() {
const actionData = useActionData<typeof action>()
const { state } = useNavigation()
return (
// Works WITHOUT JavaScript — native HTML form, server processes, redirect
<Form method="post">
{actionData?.errors?.title && <p>{actionData.errors.title[0]}</p>}
<button disabled={state === 'submitting'}>Save</button>
</Form>
)
}
Remix's <Form> submits as a native HTML form before hydration. Next.js Server Actions require JavaScript to intercept the submit event.
Caching
Next.js: Multiple caching layers (Vercel-optimized)
export const revalidate = 60 // ISR — regenerate every 60s
export const experimental_ppr = true // Partial Prerendering
// On-demand revalidation
await revalidatePath('/blog')
await revalidateTag('posts')
ISR, PPR, and on-demand revalidation are first-class on Vercel. Significantly more work on other platforms.
Remix: Standard HTTP Cache Headers
export function headers() {
return { 'Cache-Control': 'max-age=300, stale-while-revalidate=60' }
}
Works identically on every platform. No framework caching layer, no vendor dependency.
Deployment
Next.js: Optimized for Vercel. Self-hosting works but you lose ISR, edge middleware features, and on-demand revalidation without building it yourself.
Remix: Any Node.js server. One-line adapter swap between Express, Fastify, Cloudflare Workers, Fly.io, Vercel — same application code everywhere.
// Remix on Express
app.all('*', createRequestHandler({ build: require('./build') }))
// Change to Cloudflare adapter → same routes, same loaders, same actions
Decision Framework
Choose Next.js 15 if:
- Deploying to Vercel and want ISR, PPR, or Edge Middleware
- Existing Next.js codebase (migration cost isn't worth it)
- You need the largest ecosystem: auth libraries, component libraries, tutorials
- Building a content site or e-commerce where CDN caching matters
Choose Remix if:
- Progressive enhancement matters — forms that work before JavaScript loads
- Deploying to multiple platforms or need to avoid Vercel lock-in
- You want explicit data loading: route requirements visible at the top of each file
- Coming from Rails/Django — the loader/action mental model maps directly
The honest take: Next.js wins on ecosystem and CDN performance. Remix wins on web fundamentals and deployment flexibility. If you're unsure which you'll need more — ecosystem or portability — Next.js is the lower-risk starting point.
Full article at stacknotice.com/blog/nextjs-15-vs-remix-2026
Top comments (0)