小さな Workers アプリなら TanStack Start
TanStack Router を土台に Vite でビルドする React のフルスタックフレームワーク、TanStack Start の 1.0 が安定版として出ました。私は Cloudflare Workers でサイトを運用していますが、Next.js も Start も Workers に載せたことはなかったので、新しく載せるならどちらを選べばいいのか、同じアプリを両方で作って測ってみました。
npm create cloudflare で Next.js を選ぶと、1番目に「recommended」と出るのは Cloudflare が Vite の上に Next.js の API を実装し直した vinext(beta)で、本物の Next.js のビルドを Workers で動く形に変換する OpenNext は 2番目でした。Cloudflare の Next.js のガイドも vinext を既定として勧め、OpenNext は vinext に移れない既存のアプリ向けと案内しているので、Next.js は両方の道で比べました。

一覧・詳細・フォームくらいの小さなアプリを新しく作るなら、Start を選びます。OpenNext で載せた Next.js とも vinext で載せた Next.js とも比べて、Start は起動直後の応答が速く、スループットが 2.5倍以上で、ブラウザに送る JS も少なかったからです。Next.js のコードを持っているなら、まず vinext で動かし、動かない物だけ OpenNext にします。OpenNext は Next.js 16.3.8 で cacheComponents と partialPrefetching を外した形で動きました。

作ったのは一覧(20件)・詳細・追加フォームの 3つだけのアプリで、データはメモリ上の配列を同じ関数で読み、見た目もそろえて、一覧と詳細はリクエストごとにサーバーでレンダリングします。タイトルが空ならエラーを出すフォームは、Next.js は Server Action と useActionState で書きました。Start は createServerFn で作るサーバー関数を送信時に呼び、終わったら router.invalidate() でデータを取り直します。サーバー関数はサーバーだけで動き、ブラウザからは普通の関数のように呼べます。Next.js も Start も、それぞれの標準的な書き方です。


バージョンは TanStack Start 1.169.1、Next.js 16.3.8 + OpenNext 1.20.11、vinext 1.1.0 で、React 19.3.0 と wrangler 4.150.0 は共通です。数字はどれもローカルの workerd(wrangler dev と vite preview)と --dry-run で測ったもので、本番の Workers にはデプロイしていません。

新しく作るなら TanStack Start
wrangler dev を立ち上げて「Ready on」が出た 1秒後に GET / を送ると、TTFB の中央値は Start が 36ミリ秒、OpenNext で載せた Next.js が 302ミリ秒でした。ローカルで起動した直後の値で、本番のコールドスタートではありません。vinext とは両方を vite preview で測り、Start が 59ミリ秒、vinext が 96ミリ秒です。
温まった後に 10並列で 10秒リクエストを送ると、Start は約390 req/s で OpenNext の約140 req/s の 2.5倍以上でした。vite preview で比べた vinext とも 2.5倍以上の差が付きました。ブラウザが一覧ページの最初の読み込みで受け取る JS(gzip)は Start が約108KB、OpenNext が約134KB で、vinext は OpenNext より多くなりました。
クリーンビルドは Start が約3秒、OpenNext の Next.js が約21秒です。
Start を選んだら自分でやることが 2つあります。1つはサーバーでしか使わないライブラリの置き場所で、Markdown を HTML にする marked をコンポーネントの中で使うと marked がブラウザにも送られ、詳細ページの JS が 107.2KB から 118.7KB に増えました。変換をサーバー関数の中に移して HTML の文字列を返すと 107.2KB のままで、OpenNext の Next.js は Server Component に置くだけで増えませんでした。
もう1つは型チェックです。リンク切れは Start では tsc --noEmit でエラーになりますが、vite build は型チェックをしないので壊れたリンクのままビルドが通ります。CI に tsc --noEmit を入れればビルドの前に止められるはずですが、CI で止まるところまでは試していません。OpenNext の Next.js は typedRoutes: true を足すと next build が止まりました。
Next.js のコードがあるなら、まず vinext
Next.js 版の app ディレクトリを vinext にそのまま写すと動きました。最初に出たのは Next.js が生成する PageProps などのページの型が vinext には無いという tsc のエラーで、直したのは props の型だけです。大きいアプリの移行や、vinext での marked とリンク切れの扱いは試していません。
Worker のバンドル(出力の js の合計)は Start の 948KiB に対して vinext が 988KiB と同じくらいで、OpenNext の約5分の1です。ただし起動直後の応答、スループット、ブラウザの JS はどれも Start に劣り(スループットは vite preview で約140 と約370 req/s)、vinext はまだ beta です。
OpenNext は Next.js 16.3.8 で設定を 2つ外すと動いた
create-next-app の最新の Next.js 16.4.0 に OpenNext 1.20.11 を入れると、ビルドも --dry-run も通るのに、wrangler dev で開くと全部のリクエストが 500 になり、ログには次の行が出ていました。
Error: Unexpected loadManifest(/.next/server/preview-props.json) call!
16.4 のビルドが出す新しいファイルを OpenNext がまだ扱っておらず、OpenNext の peerDependencies は >=16.3.8 なので 16.4 も入ってしまいます。16.3.8 に下げても、既定の cacheComponents: true のままでは全部のリクエストが「Worker's code had hung」で 500 になり、cacheComponents と partialPrefetching を外すと 200 が返りました。Next.js を上げたら、デプロイする前に wrangler dev で開き、200 が返るかを見ます。
3つとも Workers のバンドルの上限(圧縮前 64MiB、2026年10月時点)には余裕がありますが、設定を 2つ外して動かした OpenNext は Worker のバンドルが Start の約5倍あり、起動直後の応答には約8倍の時間がかかりました。