← Blog
ArchitectureTanStack StartNext.jsDecision

Next.jsを選ばなかった理由 — TanStack Startを選んだ判断基準

更新 2026年3月13日11 分で読める

このトピックを深掘りする

TanStack Start × Cloudflare

TanStack StartとCloudflare Pagesを組み合わせたフルスタック構成・Tailwind v4移行

全4本中 1 本目。

このトピックの記事一覧

シリーズ全体の流れを見ながら、次に読む記事へ進めます。 初めての方はホームへ →

はじめに

技術選定において「何を選んだか」を語る記事は多い。しかし本当に価値があるのは「何を選ばなかったか、そしてなぜか」のほうだと思っている。選んだ理由は後からいくらでも正当化できるが、選ばなかった理由にはプロジェクト固有の制約と判断基準が凝縮されるからだ。

このサイトの技術選定において、Next.jsは最有力候補だった。Reactのフルスタックフレームワークとしては圧倒的なシェアを持ち、ドキュメントは充実し、エコシステムは成熟している。採用事例も豊富で、「とりあえずNext.js」という選択は合理的に見える。

それでもNext.jsを選ばなかった。この記事はその判断の記録だ。Next.jsを批判したいのではない。このプロジェクトの要件とNext.jsの設計思想がどこでミスマッチを起こしたのかを、できるだけ正確に言語化しておきたい。

Next.jsの検討

2026年時点のNext.jsは、App Router + React Server Components(RSC)を中心に据えた設計になっている。サーバーサイドでのデータフェッチ、ストリーミングSSR、部分的なハイドレーション——いずれも魅力的な機能だ。

特にRSCは、UIをサーバーコンポーネントとクライアントコンポーネントに分離し、サーバーで完結できる部分をクライアントに送らないというアイデアが素晴らしい。バンドルサイズの削減とパフォーマンス向上を、開発者が意識せずとも得られる。理想的な世界だ。

ただ、このプロジェクトを構成するコンポーネントを一つずつ見ていくと、その理想がうまく機能しないミスマッチが浮かび上がった。

ミスマッチ1: Vercel依存 vs エッジファースト

Next.jsはVercel上で動かしたときに最大のパフォーマンスを発揮する。ISR(Incremental Static Regeneration)、Edge Functions、Image Optimization、Analytics——これらの機能はVercelのインフラと密結合している。

もちろんNext.jsはセルフホスティングも可能だし、Cloudflare上で動かすためのアダプタも存在する。ところが実際にCloudflare Pagesに載せようとすると、制約が見えてくる。

ISRはCloudflareでは動かない。next/imageの最適化もVercel以外では自前のローダーが必要になる。Middleware APIの挙動もVercelとそれ以外で微妙に異なる。つまり、Next.jsの強みの一部は「Vercelで動かす」という暗黙の前提の上に成り立っている。

このプロジェクトでは、最初からCloudflare Pagesへのデプロイを前提としていた。理由はシンプルで、エッジロケーションの多さとコストパフォーマンスだ。211個のインタラクティブコンポーネントを載せるサイトでは、初期ロードの速さが体験の質に直結する。世界中のどこからアクセスしても、エッジから即座にレスポンスを返したかった。

ホスティングにロックインされたくなかった、というのも大きい。フレームワークの選択がホスティングの選択を制約するのは、依存の方向として不自然だと感じた。フレームワークはホスティングに対して中立であるべきだ。

ミスマッチ2: Server Components vs クライアント重心

これが最大のミスマッチだった。

このサイトには211個のインタラクティブコンポーネントがある。BubbleWrap、GravityBadges、ParticleField、FluidSimulation、PianoKeyboard——いずれもCanvas API、Web Audio API、ポインタイベント、requestAnimationFrameを駆使した、徹底的にクライアントサイドなコンポーネントだ。

RSCの恩恵を最大限に受けるのは、データベースからデータを取得して表示するような、サーバーで完結できるコンポーネントだ。ブログの記事一覧、ユーザープロフィール、商品カタログ——そういったものにRSCは絶大な効果を発揮する。

ただこのサイトの場合、サーバーで完結できるコンテンツは驚くほど少ない。プロフィール情報、スキル一覧、ソーシャルリンク——それくらいだ。全体の95%以上はクライアントサイドで動くインタラクションであり、RSCの恩恵をほとんど受けない。

Next.jsのApp Routerでは、デフォルトがServer Componentであり、クライアントサイドの機能を使うコンポーネントには 'use client' ディレクティブを付ける必要がある。211個のインタラクティブコンポーネントに 'use client' を書くことになる。

技術的に不可能ではない。だが、フレームワークが「サーバーがデフォルト」という設計思想を持っているのに、プロジェクトの大部分が「クライアントがデフォルト」であるというのは、思想のミスマッチだ。フレームワークの設計思想に逆らって使い続けるのは、長期的にフリクションを生む。

TanStack Startにはこのディレクティブの概念がない。すべてのコンポーネントは素のReactコンポーネントとして書ける。サーバーサイドの処理が必要な場合はServerFnを使うが、このプロジェクトではServerFnすら使っていない(Cloudflare Pagesとの既知の互換性問題もあったが、そもそも必要なかった)。

ミスマッチ3: ファイルベースルーティングの制約

Next.jsもTanStack Startもファイルベースルーティングを採用しているが、設計思想が異なる。

このサイトでは、211個のインタラクションを6つのカテゴリ(arcade, playground, toys, creative, music, tools)に分類し、それぞれに個別ページを持たせたかった。Next.jsの場合、動的ルート [slug]/page.tsx を使えば実現できる。これはTanStack Startでも同様だ。

違いが出るのは型安全性の部分だ。TanStack Routerは、ルートパラメータとサーチパラメータに対して完全な型推論を提供する。

// TanStack Router の型安全なルート定義
export const Route = createFileRoute('/playground/$slug')({
  component: PlaygroundPage,
  head: ({ params }) => ({
    // params.slug は string 型として推論される
    meta: [{ title: getComponentTitle(params.slug) }],
  }),
})

params.slugstring 型として推論され、存在しないパラメータにアクセスしようとするとコンパイルエラーになる。サーチパラメータについても同様で、validateSearch でZodスキーマを定義すれば、クエリパラメータの型が自動的に推論される。

Next.jsでも params の型定義は可能だが、TanStack Routerほどの型推論の深さはない。211ページ分のルーティングを型安全に管理する場合、この差は無視できない。

ルートツリーが自動生成される点も良い。src/routeTree.gen.ts が自動的に生成され、すべてのルートの型情報を一元管理してくれる。新しいルートファイルを追加すれば、ルートツリーが自動更新される。手動でルート定義を書く必要がない。

TanStack Startを選んだ理由

3つのミスマッチを踏まえて、TanStack Startを選んだ理由を整理する。

Viteベースであること。 TanStack StartはViteの上に構築されている。Cloudflare Pagesとの統合は @cloudflare/vite-plugin をプラグインに追加するだけで完了する(プラグインの順序に罠はあったが、それは別の記事に書いた)。WebpackベースのNext.jsと比べて、ビルドパイプラインがシンプルで見通しが良い。

Type-safe routing。 前述の通り、ルートパラメータとサーチパラメータの型推論が秀逸だ。211ページのルーティングを管理するうえで、型安全性は開発速度と品質の両方に直結する。

head() API。 SEO、OGP、JSON-LDの管理が宣言的に書ける。各ルートのhead情報をルートファイル内に閉じ込められるので、メタ情報の管理が散らばらない。

head: ({ params }) => ({
  meta: [
    { title: `\${component.title} | sakimyto` },
    { name: 'description', content: component.description },
    { property: 'og:title', content: component.title },
  ],
  links: [
    { rel: 'canonical', href: `https://sakimyto.com/playground/\${params.slug}` },
  ],
  scripts: [
    {
      type: 'application/ld+json',
      children: JSON.stringify(generateJsonLd(component)),
    },
  ],
})

バンドルサイズ。 フレームワーク自体のサイズがNext.jsより小さい。211個のインタラクティブコンポーネントはそれ自体がかなりのバンドルサイズを持つため、フレームワーク層は可能な限り軽量であってほしかった。

フレームワークの思想がプロジェクトの要件と合致した。 TanStack Startは「サーバーサイドもできるクライアントフレームワーク」であり、Next.jsは「クライアントサイドもできるサーバーフレームワーク」だ。このプロジェクトの重心はクライアントサイドにあるから、前者のほうが自然だった。

トレードオフ

もちろんトレードオフはある。隠すつもりはない。

エコシステムの成熟度。 Next.jsのエコシステムは圧倒的だ。認証(NextAuth.js)、CMS連携、データベースORM——Next.js向けに最適化されたツールが無数にある。TanStack Startにはそれがない。必要なものは自分で組み合わせる必要がある。

ドキュメントの充実度。 Next.jsのドキュメントは体系的で、チュートリアルからAPIリファレンスまで網羅されている。TanStack Startのドキュメントは、v1リリース時点ではまだ薄い部分が多かった。特にCloudflare Pagesとの統合については、ほぼ手探りだった。

「ドキュメントに書いてないこと」を自力で解決する覚悟。 実際に踏んだ地雷は少なくない。Viteプラグインの順序問題(tailwindcss → tsConfigPaths → cloudflare → tanstackStart → react)、ServerFnのCloudflare Pages非互換、HMRの不安定さ——いずれもGitHubのIssueとソースコードを読んで自力で解決した。

コミュニティのサイズ。 何か問題が起きたとき、Stack Overflowで検索しても答えが見つからないことが多い。GitHubのIssueとDiscussionが主な情報源になる。「みんなが使っている」という安心感はない。

これらのトレードオフは、このプロジェクトでは許容できた。認証もCMS連携もデータベースも不要で、全データは静的なTypeScript定数として src/data/ に置いている。ドキュメントの不足は、ソースコードを読む力でカバーできると判断した。

とはいえ、チーム開発やビジネスクリティカルなプロジェクトでは、この判断は変わるかもしれない。エコシステムとコミュニティの厚さは、長期運用における保険として無視できない価値がある。

まとめ:Next.js vs TanStack Start

技術選定は「プロジェクトの重心」で決めるべきだと思っている。

このプロジェクトの重心は明確だった。クライアントサイドインタラクションエッジデプロイ。211個のインタラクティブコンポーネントがサイトの本質であり、それをCloudflare Pagesのエッジから世界中に配信する。サーバーサイドで処理すべきデータはほとんどない。

Next.jsの重心は「サーバーサイドレンダリング」と「Vercelプラットフォーム」にある。それは素晴らしい設計思想であり、多くのプロジェクトにとって最適解だ。しかし、このプロジェクトの重心とは合わなかった。

TanStack Startの重心は「Viteベースの軽量フレームワーク」と「型安全なルーティング」にある。クライアントサイドを主体としつつ、必要に応じてサーバーサイドの機能も使える。このプロジェクトの重心と一致した。

重要なのは、Next.jsが悪いフレームワークだということではない。どんなフレームワークにも設計思想があり、その思想がプロジェクトの要件と合致するかどうかが技術選定の本質だ。

「最も人気のあるフレームワーク」が「最も適したフレームワーク」とは限らない。プロジェクトの重心を見極め、その重心と設計思想が合致するフレームワークを選ぶ。技術選定とは、つまるところそういう作業だと思う。