Next.js 16 vs Remix
Vercel Next.js 16 versus Shopify Remix: server components, data loading, deployment, performance, and the modern React full-stack framework choice.
A React layer pre-rendered on the server, zero-JS by default
The classic, mature way of using React, rendered in the browser
On content-heavy, SEO-critical surfaces, RSC delivers a measurable win: in react.dev's own example, a 75K-gzip library never reaches the client at all. On heavily interactive internal panels (dashboard, admin), client-first is both simpler and exposes fewer error surfaces. The critical warning is real: passing a full object prop from Server to Client writes all of that object's fields into the flight payload — draw the boundary wrong and the win quietly erodes. Make the call by measurement, not by trend.
| Category | React Server Components | Client-side React (SPA) |
|---|---|---|
| Performance | 8/10 | 6/10 |
| Ease of Learning | 4/10 | 8/10 |
| Ecosystem | 7/10 | 9/10 |
| Community | 7/10 | 9/10 |
| Job Market | 7/10 | 8/10 |
| Future-Proof | 9/10 | 7/10 |
// Next.js App Router — Server Component (default)
// app/posts/[slug]/page.tsx
import { db } from '@/lib/db';
import LikeButton from './like-button'; // Client Component (in a separate file, 'use client')
interface PageProps {
params: Promise<{ slug: string }>;
}
export default async function PostPage({ params }: PageProps) {
const { slug } = await params;
// Direct await on the server — no client-server waterfall,
// heavy libraries like marked/sanitize-html never reach the client bundle
const post = await db.post.findUnique({ where: { slug } });
if (!post) return <div>Not found</div>;
return (
<article>
<h1>{post.title}</h1>
{/* Only serializable props are passed to the Client Component */}
<div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
<LikeButton postId={post.id} initialCount={post.likeCount} />
</article>
);
}
// app/posts/[slug]/like-button.tsx
'use client';
import { useState, useTransition } from 'react';
export default function LikeButton({ postId, initialCount }: { postId: string; initialCount: number }) {
const [count, setCount] = useState(initialCount);
const [isPending, startTransition] = useTransition();
return (
<button
disabled={isPending}
onClick={() => startTransition(async () => {
setCount((c) => c + 1);
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
})}
>
Like ({count})
</button>
);
}// Vite + React — Client-side SPA
// src/pages/PostPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
interface Post {
id: string;
title: string;
renderedHtml: string;
likeCount: number;
}
export default function PostPage() {
const { slug } = useParams();
const [post, setPost] = useState<Post | null>(null);
const [isLiking, setIsLiking] = useState(false);
useEffect(() => {
// All data fetching happens on the client — this is where waterfall risk begins
let cancelled = false;
fetch(`/api/posts/${slug}`)
.then((res) => res.json())
.then((data) => { if (!cancelled) setPost(data); });
return () => { cancelled = true; };
}, [slug]);
if (!post) return <div>Loading...</div>; // This screen never shows without JS running
async function handleLike() {
setIsLiking(true);
setPost((p) => (p ? { ...p, likeCount: p.likeCount + 1 } : p));
await fetch(`/api/posts/${post!.id}/like`, { method: 'POST' });
setIsLiking(false);
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
<button disabled={isLiking} onClick={handleLike}>
Like ({post.likeCount})
</button>
</article>
);
}
// vite.config.ts — code-splitting is delegated to Rollup
export default {
build: {
rollupOptions: {
output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router'] } },
},
},
};On content-heavy, SEO-critical surfaces, RSC delivers a measurable win: in react.dev's own example, a 75K-gzip library never reaches the client at all. On heavily interactive internal panels (dashboard, admin), client-first is both simpler and exposes fewer error surfaces. The critical warning is real: passing a full object prop from Server to Client writes all of that object's fields into the flight payload — draw the boundary wrong and the win quietly erodes. Make the call by measurement, not by trend.
Get Free ConsultationOn content-heavy, SEO-critical surfaces where first load (LCP/TTI) matters (blog, product page, comparison page), RSC delivers a measurable win — server-side data fetching and less client JS. According to Next.js 16.3's own numbers, switching server-side rendering to Node.js native streams handles 22% more requests under load. On heavily interactive internal panels (dashboard, admin), client-first usually offers a simpler development experience.