next vs remix 徹底比較

next の詳細remix の詳細
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

Next.js vs Remix — どちらのReactフレームワークを選ぶべきか?

1. 結論

既存のReactエコシステムとの統合性・Vercelとの親和性・豊富な実績を重視するなら Next.js を選んでください。Web標準API への準拠・ネストルーティングによるデータフェッチの最適化・プログレッシブエンハンスメントを重視するなら Remix が適しています。どちらも本番運用に耐えうる成熟したフレームワークですが、プロジェクトの性質とチームの志向によって最適解が変わります。


2. 比較表

観点Next.js (next)Remix (remix / @remix-run/*)
最新安定バージョン(2025年6月時点)v15.xv2.x(React Router v7 に統合進行中)
npm 週間DL数約 700万+約 30万+
バンドルサイズ(クライアント最小構成)やや大きい(RSC含む)比較的軽量
TypeScript対応◎ ファーストクラス◎ ファーストクラス
レンダリング戦略SSR / SSG / ISR / RSC / CSRSSR / CSR(SSG は限定的)
ルーティングファイルベース(App Router / Pages Router)ファイルベース(ネストルーティング)
データフェッチServer Components / fetch / Route Handlersloader / 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標準に近いため経験者は馴染みやすい)
エコシステム・コミュニティ◎ 非常に大きい○ 成長中
開発元VercelShopify(旧 Remix Software)
ライセンスMITMIT

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 / revalidateTagaction 後に 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 による自動最適