Next.js vs Remix — どちらのReactフレームワークを選ぶべきか?
1. 結論
既存のReactエコシステムとの統合性・Vercelとの親和性・豊富な実績を重視するなら Next.js を選んでください。Web標準API への準拠・ネストルーティングによるデータフェッチの最適化・プログレッシブエンハンスメントを重視するなら Remix が適しています。どちらも本番運用に耐えうる成熟したフレームワークですが、プロジェクトの性質とチームの志向によって最適解が変わります。
2. 比較表
| 観点 | Next.js (next) | Remix (remix / @remix-run/*) |
|---|---|---|
| 最新安定バージョン(2025年6月時点) | v15.x | v2.x(React Router v7 に統合進行中) |
| npm 週間DL数 | 約 700万+ | 約 30万+ |
| バンドルサイズ(クライアント最小構成) | やや大きい(RSC含む) | 比較的軽量 |
| TypeScript対応 | ◎ ファーストクラス | ◎ ファーストクラス |
| レンダリング戦略 | SSR / SSG / ISR / RSC / CSR | SSR / CSR(SSG は限定的) |
| ルーティング | ファイルベース(App Router / Pages Router) | ファイルベース(ネストルーティング) |
| データフェッチ | Server Components / fetch / Route Handlers | loader / action(Web Fetch API準拠) |
| フォーム処理 | Server Actions(RSC) | <Form> + action(プログレッシブエンハンスメント) |
| ミドルウェア | middleware.ts(Edge Runtime) | なし(loader/action で代替) |
| デプロイ先 | Vercel最適化 / Node / Docker / 各種アダプタ | Node / Cloudflare / Deno / Fly.io 等アダプタ |
| 静的サイト生成(SSG) | ◎ 強力 | △ 限定的(v2 で改善中) |
| 学習コスト | 中〜高(App Router + RSC の概念が複雑) | 中(Web標準に近いため経験者は馴染みやすい) |
| エコシステム・コミュニティ | ◎ 非常に大きい | ○ 成長中 |
| 開発元 | Vercel | Shopify(旧 Remix Software) |
| ライセンス | MIT | MIT |
3. それぞれの強み
Next.js の強み
- レンダリング戦略の豊富さ: SSR・SSG・ISR・RSC(React Server Components)を1つのプロジェクト内でページ単位に使い分けられます。ブログのような静的ページとダッシュボードのような動的ページを同居させるのが容易です。
- React Server Components(RSC)の先行実装: クライアントバンドルを削減しつつサーバーサイドでコンポーネントをレンダリングする最新のReactアーキテクチャをいち早く採用しています。
- Vercel との統合: デプロイ・プレビュー・Edge Functions・Analytics・Image Optimization など、Vercel プラットフォームとシームレスに連携します。
- 圧倒的なエコシステム: Auth.js (NextAuth)、Prisma、tRPC、Contentlayer など、Next.js を前提としたライブラリやチュートリアルが豊富です。
- ISR(Incremental Static Regeneration): 静的生成の恩恵を受けつつ、バックグラウンドで再生成することで鮮度を保てます。
Remix の強み
- Web標準への忠実さ:
Request/Response/FormData/Headersなど、Web Fetch API をそのまま使います。フレームワーク固有の抽象化が少なく、学んだ知識がフレームワーク外でも活きます。 - ネストルーティングと並列データフェッチ: ルートの階層ごとに
loaderを定義し、親子ルートのデータを並列に取得します。ウォーターフォール問題を構造的に解消できます。 - プログレッシブエンハンスメント:
<Form>コンポーネントは JavaScript が無効でも HTML のフォーム送信として動作し、JS が有効なら fetch に自動昇格します。 - エラーバウンダリの粒度: ルートセグメントごとに
ErrorBoundaryを定義でき、エラーが発生したセグメントだけを差し替えて他の部分は正常に表示し続けられます。 - デプロイ先の柔軟性: アダプタ方式により Cloudflare Workers・Deno・Fly.io など Edge 環境への対応が自然です。
4. コード例で比較
課題: 「ユーザー一覧を取得して表示し、新規ユーザーをフォームから追加する」
Next.js(App Router / Server Components + Server Actions)
project/
├── app/
│ └── users/
│ ├── page.tsx
│ └── actions.ts
└── lib/
└── db.ts
lib/db.ts(共通のダミーDB)
// lib/db.ts
export interface User {
id: number;
name: string;
email: string;
}
// 簡易インメモリDB(デモ用)
let users: User[] = [
{ id: 1, name: "田中太郎", email: "tanaka@example.com" },
{ id: 2, name: "佐藤花子", email: "sato@example.com" },
];
export function getUsers(): User[] {
return [...users];
}
export function addUser(name: string, email: string): User {
const newUser: User = { id: users.length + 1, name, email };
users.push(newUser);
return newUser;
}
app/users/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { addUser } from "@/lib/db";
export async function createUserAction(formData: FormData): Promise<void> {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
if (!name || !email) {
throw new Error("名前とメールアドレスは必須です");
}
addUser(name, email);
revalidatePath("/users");
}
app/users/page.tsx
// app/users/page.tsx — Server Component(デフォルト)
import { getUsers } from "@/lib/db";
import { createUserAction } from "./actions";
export default function UsersPage() {
// サーバー側で直接データ取得(fetch不要)
const users = getUsers();
return (
<main style={{ maxWidth: 600, margin: "0 auto", padding: 24 }}>
<h1>ユーザー一覧(Next.js)</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
<h2>ユーザー追加</h2>
<form action={createUserAction}>
<div>
<label htmlFor="name">名前: </label>
<input id="name" name="name" type="text" required />
</div>
<div style={{ marginTop: 8 }}>
<label htmlFor="email">メール: </label>
<input id="email" name="email" type="email" required />
</div>
<button type="submit" style={{ marginTop: 12 }}>
追加
</button>
</form>
</main>
);
}
ポイント:
page.tsxは Server Component なのでasyncにもでき、直接 DB/ORM を呼べます"use server"で Server Action を定義し、<form action={...}>にバインドしますrevalidatePathでキャッシュを無効化し、一覧を再描画します
Remix(v2 / loader + action)
project/
├── app/
│ ├── routes/
│ │ └── users.tsx
│ └── lib/
│ └── db.server.ts
app/lib/db.server.ts
// app/lib/db.server.ts
// ファイル名に .server を付けるとクライアントバンドルから除外される
export interface User {
id: number;
name: string;
email: string;
}
let users: User[] = [
{ id: 1, name: "田中太郎", email: "tanaka@example.com" },
{ id: 2, name: "佐藤花子", email: "sato@example.com" },
];
export function getUsers(): User[] {
return [...users];
}
export function addUser(name: string, email: string): User {
const newUser: User = { id: users.length + 1, name, email };
users.push(newUser);
return newUser;
}
app/routes/users.tsx
// app/routes/users.tsx
import type {
LoaderFunctionArgs,
ActionFunctionArgs,
} from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { useLoaderData, Form } from "@remix-run/react";
import { getUsers, addUser } from "~/lib/db.server";
// --------- loader: GET時にサーバーで実行 ---------
export async function loader({ request }: LoaderFunctionArgs) {
const users = getUsers();
return json({ users });
}
// --------- action: POST/PUT/DELETE時にサーバーで実行 ---------
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const name = formData.get("name") as string;
const email = formData.get("email") as string;
if (!name || !email) {
return json(
{ error: "名前とメールアドレスは必須です" },
{ status: 400 }
);
}
addUser(name, email);
// action 完了後、同じルートの loader が自動再実行される
return redirect("/users");
}
// --------- コンポーネント ---------
export default function UsersRoute() {
const { users } = useLoaderData<typeof loader>();
return (
<main style={{ maxWidth: 600, margin: "0 auto", padding: 24 }}>
<h1>ユーザー一覧(Remix)</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
<h2>ユーザー追加</h2>
{/* Remix の <Form> は JS有効時は fetch、無効時は通常のフォーム送信 */}
<Form method="post">
<div>
<label htmlFor="name">名前: </label>
<input id="name" name="name" type="text" required />
</div>
<div style={{ marginTop: 8 }}>
<label htmlFor="email">メール: </label>
<input id="email" name="email" type="email" required />
</div>
<button type="submit" style={{ marginTop: 12 }}>
追加
</button>
</Form>
</main>
);
}
ポイント:
loader/actionという明確な規約でデータの読み書きを分離します<Form method="post">が action をトリガーし、完了後に loader が自動再実行されるため手動のキャッシュ無効化が不要ですrequest.formData()は Web 標準の API そのものです
コード比較のまとめ
| 観点 | Next.js (App Router) | Remix (v2) |
|---|---|---|
| データ取得 | Server Component 内で直接呼び出し | loader 関数で json() を返す |
| データ変更 | Server Actions ("use server") | action 関数 + <Form> |
| 再描画トリガー | revalidatePath / revalidateTag | action 後に loader が自動再実行 |
| JS無効時の動作 | Server Actions は JS 必須(※ <form> fallback は限定的) | <Form> が HTML フォームにフォールバック |
| 型安全性 | Server Actions の引数は FormData(zodなどで補強) | useLoaderData<typeof loader>() で推論 |
5. どちらを選ぶべきか — ユースケース別ガイド
Next.js を選ぶべきケース
| ユースケース | 理由 |
|---|---|
| コンテンツ中心のサイト(ブログ・LP・ドキュメント) | SSG / ISR による高速配信と優れたビルドキャッシュ |
| Vercel にデプロイする前提のプロジェクト | ゼロコンフィグで最大限の最適化が得られる |
| 大規模チーム・長期運用 | エコシステムの大きさ、採用実績、情報量の多さが安定運用を支える |
| React Server Components を活用したい | RSC のファーストクラスサポート |
| 画像最適化が重要(EC・メディア) | next/image による自動最適 |