Next.js と TanStack Start の RSC の比較と注意点
1. 基盤アーキテクチャの違い🔗
ビルドやサーバーの基盤部分からして、両者の設計思想は大きく異なっています。
| 項目 | Next.js (App Router) | TanStack Start |
|---|---|---|
| ビルドエンジン | webpack / Turbopack | Vite |
| コンパイラ / 統合 | SWC (Next.js独自拡張) | Vite Plugin (@tanstack/react-start/plugin/vite) |
| サーバー基盤 | Node.js / Edge Runtime (Vercel環境に最適化) | Viteを中心に、必要に応じてNitroなどをプラグインとして統合 |
| ルーティング | ファイルシステムベースのディレクトリ構造 | ファイルシステムベースのルーティング+強力な定義オブジェクト |
Next.jsは、コンパイラ(SWC)やビルドエンジン(webpack/Turbopack)をフレームワーク自身がコントロールする垂直統合型です。Node.jsやEdge Runtimeで動作し、特にVercelでのホスティングに最適化されています。静的生成(SSG)やISR周りの最適化は流石の完成度を誇ります。
対するTanStack Startは、現在はViteプラグインとして提供されており、vite.config.ts に @tanstack/react-start/plugin/vite を組み込んで利用します。以前はVinxiベースの構成でしたが、現在のReact StartではViteへの統合が進み、通常の開発・ビルドコマンドも vite dev / vite build を使う形になっています。Next.js特有の独自ルールに縛られず、Viteプラグインなどの豊富なエコシステムをそのまま使えるのが強みになっています。
注: TanStack Startの初期の情報では「Vinxiベース」「NitroをVinxi経由で利用」と説明されていることがありますが、現在の公式ドキュメントではViteプラグイン中心の構成に移行しています。Nitroも必要に応じて
nitro/viteプラグインとして追加する扱いです。
2. ルーティングとRSC・データフェッチの挙動🔗
両者ともファイルベースルーティングを採用していますが、ルーターの起点となるコンポーネントの扱いやデータ取得の方法には根本的な違いがあります。
起点コンポーネントが Server か Client か🔗
RSCを扱う上での一番のメンタルモデルの違いは、ページの入り口となるルートコンポーネントの性質です。
Next.jsは、ルート(page.tsx や layout.tsx)がデフォルトで Server Component になります。サーバー側が起点となり、必要に応じてクライアント機能が必要な末端のコンポーネントで "use client" を宣言していくトップダウンの構造です。
一方でTanStack Startは、ルート(Route)に指定するコンポーネントがデフォルトで Client Component です。SPAファーストで設計されたTanStack Routerが土台にあるため、ルートコンポーネント自体はクライアントとして動作し、データのロード(loader)やサーバー関数(createServerFn)を通じて必要な部分だけをサーバー側(RSC)と連携させます。
この設計の違いにより、データフェッチのコードの書き方やレンダリング戦略も大きく異なります。
Next.js (App Router):コンポーネント内での直接フェッチ🔗
Next.jsは、Server Componentが非同期関数(async/await)になれる特性を活かして、コンポーネント内で直接 fetch を呼ぶスタイルが基本です。
1// src/app/users/[id]/page.tsx2import { notFound } from "next/navigation";34interface User {5 id: string;6 name: string;7}89async function getUser(id: string): Promise<User> {10 const res = await fetch(`https://api.example.com/users/${id}`, {11 next: { revalidate: 60 }, // Next.js独自のキャッシュ拡張12 });13 if (!res.ok) throw new Error("User not found");14 return res.json();15}1617export default async function UserPage({18 params,19}: {20 params: Promise<{ id: string }>;21}) {22 const { id } = await params;23 const user = await getUser(id);2425 return (26 <div>27 <h1>{user.name}</h1>28 </div>29 );30}
TanStack Start:loader と createServerFn🔗
TanStack Startでは、データ取得はルーター側の loader に集約されます。また、サーバー側で処理を実行するための「サーバー関数」として createServerFn を使用します。
1// src/routes/users.$id.tsx2import { createFileRoute } from "@tanstack/react-router";3import { createServerFn } from "@tanstack/react-start";4import { z } from "zod";56// サーバー関数 (ネットワーク境界を越えて自動でAPIエンドポイント化される)7const getUser = createServerFn({ method: "GET" })8 .validator((id: string) => id)9 .handler(async ({ data: id }) => {10 const res = await fetch(`https://api.example.com/users/${id}`);11 if (!res.ok) throw new Error("User not found");12 return res.json() as Promise<{ id: string; name: string }>;13 });1415export const Route = createFileRoute("/users/$id")({16 params: {17 parse: (params) => ({18 id: z.string().parse(params.id),19 }),20 },21 loader: async ({ params }) => {22 const user = await getUser({ data: params.id });23 return { user };24 },25 component: UserComponent,26});2728function UserComponent() {29 const { user } = Route.useLoaderData();3031 return (32 <div>33 <h1>{user.name}</h1>34 </div>35 );36}
Server Componentとストリーミングの挙動🔗
TanStack StartでもServer Componentを扱うことはできますが、現時点では実験的な位置づけが強く、Next.jsのApp Routerと同じ感覚でルート全体をServer Component中心に組み立てるものではありません。公式ドキュメントでも、renderServerComponent をサーバー関数から返し、ルートの loader 経由で受け取るようなパターンが紹介されています。
また、TanStack StartのServer ComponentはSelective SSRやStreaming SSRと組み合わせて使う設計になっています。非同期なServer ComponentをSuspense境界の内側で扱うと、準備できた部分から段階的に返すストリーミング寄りの挙動になります。
UXとしては非常に面白い仕組みですが、SEOの観点で「初期HTMLにすべてのデータを含めたい(同期的なSSRをしたい)」という場面では、どこでデータを解決するかを明確に設計する必要があります。データを含んだHTMLを安定して返したい場合は、コンポーネント内での直接フェッチに寄せすぎず、ルーターの loader でフェッチを解決して useLoaderData で同期的に描画するパターンを優先すると扱いやすくなります。
比較のポイント🔗
Next.jsはコンポーネントにデータ取得のロジックを閉じ込められるのが手軽です。同一リクエスト内の fetch はメモ化されるなどの仕組みもありますが、親のデータ取得結果に依存して子がさらにフェッチするような書き方では、ウォーターフォール(数珠つなぎの遅延)が起きやすいという注意点があります。
TanStack Startは、ページ遷移前に loader でデータをプリフェッチするためウォーターフォールを回避しやすく、遷移時のローディング表示などもルーター側で綺麗に制御できます。
3. 型安全性(Type Safety)の比較🔗
TypeScriptで開発する上で、両者の型安全性のレベルにはかなり大きな差があります。
Next.jsの型安全性の限界🔗
Next.js(App Router)のルーティングは、パスパラメータやクエリパラメータを型安全に保つのがやや苦手です。/users/[id] の id は単なる文字列として扱う場面が多く、バリデーションやキャストを個別に実装する必要があります。リンクの遷移先については typedRoutes を有効化すれば一定の型チェックを受けられますが、search paramsの型やパラメータのバリデーションまで含めて一貫して保証する仕組みではありません。
TanStack Startの徹底した型安全🔗
TanStack Startの型安全性はかなり強力です。パスパラメータの段階で zod を使ったバリデーションを設定でき、パースされた正しい型がコンポーネントで自動推論されます。また、Link に指定する遷移先やパラメータの指定漏れもTypeScriptがコンパイルエラーとして即座に検知してくれます。
さらに、createServerFn で定義したサーバー関数の引数と戻り値の型が、ネットワークの境界を越えてクライアント側にも追従するため、APIの型定義を別で用意する手間を大きく減らせます。このシームレスな開発体験は非常に快適です。
4. RSC機能の比較まとめ🔗
Next.jsとTanStack StartにおけるRSCおよび関連機能の比較を以下にまとめます。
| 比較項目 | Next.js (App Router) | TanStack Start |
|---|---|---|
| ルート起点(エントリポイント)の性質 | デフォルトで Server Component。必要に応じてクライアントに切り替える。 | デフォルトで Client Component。必要に応じてサーバー関数やRSCを呼ぶ。 |
コンポーネント内での非同期処理 (async/await) | 標準的なパターン。Suspenseやキャッシュと組み合わせて制御する。 | RSCではSelective SSRやStreaming SSRと組み合わせて扱う。 |
| 推奨データフェッチパターン | Server Component内での直接フェッチ(API呼び出しやDBクエリ)。 | ルーターの loader で取得し、useLoaderData() で同期描画。 |
| サーバー関数(RSC境界) | Server Actions ("use server") | Server Functions (createServerFn()) |
| 境界を跨ぐ型安全性 | 疎結合(paramsの型やActionsのシリアライズ等は型推論が弱い)。 | 強力(validatorやServer Functionsを通じて型がつながりやすい)。 |
| キャッシュ制御 | fetch 拡張(revalidate)や独自キャッシュ(挙動が複雑)。 | 通常のフェッチ、あるいはTanStack Query等と連携(シンプル)。 |
5. 移行や開発における注意点・デメリット🔗
どちらも魅力的なフレームワークですが、実際にプロジェクトへ導入するとなるといくつかの懸念点があります。
Next.jsの懸念:複雑なキャッシュとホスティングの制約🔗
Next.jsのキャッシュは、パフォーマンスの最適化に役立つものの、挙動を理解するには慣れが必要です。fetch のData Cache、同一リクエスト内のRequest Memoization、Full Route Cache、Router Cacheなど複数の層があり、Next.js 15以降では fetch のデフォルトキャッシュ挙動も変わっています。「キャッシュがなぜか更新されない」「意図しないデータが表示される」といった問題を避けるには、どの層のキャッシュを扱っているのかを意識する必要があります。
また、画像最適化やISRといった機能をフル活用しようとするとVercelでの運用がもっとも楽です。自前のサーバーや他のクラウド(AWSやCloudflareなど)で動かすこともできますが、一部機能の再現や運用設計には追加の検討が必要になります。
TanStack Startの懸念:成熟度とエコシステムの問題🔗
TanStack Startはまだ活発に開発が進められている段階のため、ドキュメントが十分でなかったり、マイナーアップデートで破壊的変更が入ったりすることがあります。ネット上の情報もNext.jsに比べるとまだ多くありません。
また、ReactのUIコンポーネントライブラリの中にはNext.jsでの動作を前提に作られているものがあります。そのため、Viteベースのビルド環境でインポートすると、「use client が宣言されていない」などの理由でビルドエラーになることがあり、Viteの設定を調整するワークアラウンドが必要になる場面があります。さらに、TanStack Start本体だけでなく、Viteプラグイン、Nitroなどの周辺ツールも関わるため、トラブルシューティング時の学習コストが高いのも難点です。
6. まとめ:どちらを採用すべきか?🔗
手堅くプロジェクトを進めたい場合や、サードパーティのUIライブラリを多く使う予定があるなら、情報量が豊富でエコシステムが成熟しているNext.jsが無難な選択肢になります。Vercelを使って手早くデプロイしたい場合もNext.jsが第一候補です。
一方で、Viteによる高速な開発体験(HMR)が欲しい、徹底的に型安全なコードを書きたい、あるいはCloudflare Workersなどのサーバーレス環境にベンダーロックインなしでデプロイしたいといった場合には、TanStack Startは非常に有力な選択肢です。
RSCのあり方はNext.jsが先導してきましたが、TanStack Startというもう一つのアプローチが登場したことで、より多様な設計が選べるようになりました。ただし、TanStack StartのRSC関連機能は変化が速いため、採用時には公式ドキュメントとサンプルの更新状況を確認するのがおすすめです。それぞれの特徴と開発体験の違いを考慮して、プロジェクトに合ったフレームワークを選定してみてください。
参考リンク🔗
- TanStack Start: Server Components
- TanStack Start: Server Functions
- TanStack Start: Hosting
- Next.js: Fetching Data
- Next.js: typedRoutes