Next.js 16 vs Remix
Next.js 16(Turbopackデフォルト、Partial Prerendering)vs Remix(Shopify、Web標準)——2026年のReactメタフレームワーク比較。
サーバーで事前レンダリングされる、ゼロJSがデフォルトのReactレイヤー
ブラウザでレンダリングされる、クラシックで成熟したReact利用形態
コンテンツ量が多く、SEOが重要な面ではRSCが測定可能な利益をもたらします:react.dev公式の例では75Kのgzipライブラリがclientに一切送られません。高インタラクティブな内部パネル(dashboard、admin)ではclient-firstの方がシンプルでエラー面も少なく済みます。重要な注意点は現実のものです:ServerからClientへオブジェクトのpropをそのまま渡すと、そのオブジェクトの全フィールドがflight payloadに書き込まれます — 境界を誤って引くと利益が静かに失われます。決定は流行ではなく測定で下してください。
| カテゴリー | React Server Components | Client-side React (SPA) |
|---|---|---|
| パフォーマンス | 8/10 | 6/10 |
| 学習のしやすさ | 4/10 | 8/10 |
| エコシステム | 7/10 | 9/10 |
| コミュニティ | 7/10 | 9/10 |
| 求人市場 | 7/10 | 8/10 |
| 将来性 | 9/10 | 7/10 |
// Next.js App Router — Server Component(デフォルト)
// app/posts/[slug]/page.tsx
import { db } from '@/lib/db';
import LikeButton from './like-button'; // Client Component(別ファイルで 'use client')
interface PageProps {
params: Promise<{ slug: string }>;
}
export default async function PostPage({ params }: PageProps) {
const { slug } = await params;
// サーバー側で直接await — client-server waterfallなし、
// marked/sanitize-htmlのような重いライブラリはclient bundleに一切含まれない
const post = await db.post.findUnique({ where: { slug } });
if (!post) return <div>見つかりません</div>;
return (
<article>
<h1>{post.title}</h1>
{/* シリアライズ可能なpropのみが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' });
})}
>
いいね ({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(() => {
// すべてのデータ取得はclient側 — waterfallリスクはここから始まる
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>読み込み中...</div>; // JS実行前にこの画面は一切表示されない
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}>
いいね ({post.likeCount})
</button>
</article>
);
}
// vite.config.ts — code-splittingはRollupに委ねられる
export default {
build: {
rollupOptions: {
output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router'] } },
},
},
};コンテンツ量が多く、SEOが重要な面ではRSCが測定可能な利益をもたらします:react.dev公式の例では75Kのgzipライブラリがclientに一切送られません。高インタラクティブな内部パネル(dashboard、admin)ではclient-firstの方がシンプルでエラー面も少なく済みます。重要な注意点は現実のものです:ServerからClientへオブジェクトのpropをそのまま渡すと、そのオブジェクトの全フィールドがflight payloadに書き込まれます — 境界を誤って引くと利益が静かに失われます。決定は流行ではなく測定で下してください。
無料相談を受けるコンテンツ量が多く、SEOが重要で、初回ロード(LCP/TTI)が重要な面(ブログ、製品ページ、比較ページ)では、RSCが測定可能な利益をもたらします — サーバー側でのデータ取得とclient JSの削減です。Next.js 16.3自身のデータによると、サーバー側レンダリングでNode.jsネイティブstreamへの移行により、負荷時に22%多くのリクエストを処理できます。高インタラクティブな内部パネル(dashboard、admin)では、client-firstの方が一般的にシンプルな開発体験を提供します。