
長年、WebCatalogではWeb上のほぼすべてのものをVercelにデプロイしていました。プロジェクトの大半がNext.jsを使っていたので、自然な組み合わせでした。マーケティングサイト、製品のインターフェース、アカウント設定、開発者コンソール、認証、管理ツール、APIは、どれもほぼ同じ技術スタック上にありました。
それは長い間うまく機能していました。Vercelのおかげでデプロイは簡単になり、Next.jsは生産性の高いフルスタックフレームワークを提供してくれました。その下にあるインフラを意識する必要はほとんどありませんでした。
しかし、やがてその利便性が私たちの製品の実態に合わなくなりました。
マーケティングサイトには今も**SSR(サーバーサイドレンダリング)のメリットがありましたが、それ以外のWebアプリの大半には必要ありませんでした。単純なSPA(シングルページアプリケーション)**で十分だったのです。APIには、ネイティブ依存関係を利用でき、プロセスを長時間稼働させられる通常のNode.js環境が必要でした。フロントエンドの技術スタックでは、それ以外のほぼすべてですでにViteを使っていました。同時に、一般公開サイトへのアクセスや自動化されたトラフィックが増えるにつれ、インフラコストも無視できなくなっていました。
最初は、既存の構成を最適化しようとしました。Next.jsを維持し、アダプターを介してCloudflare Workersに移すことを検討しました。マーケティングサイトにはAstroを試しました。APIをCloudflare Containers上で動かした時期も短期間ありました。
最終的には、もっとシンプルな構成に落ち着きました。現在、Webアプリの大半はCloudflare Workers上のVite + TanStack Router、マーケティングサイトはCloudflare Workers上のSSR対応TanStack Start、APIはRailway上で長時間稼働するNode.jsコンテナによるHono + tRPCを使っています。
この移行によって、Webインフラのコストは約**80%削減されました。さらにCloudflareのセキュリティルールを強化し、悪意のある自動化トラフィックをエッジでより多くブロックした結果、削減率は全体で約90%**に達しました。
移行に伴う実装作業の大部分もAIコーディングエージェント、主にClaude Fable 5とClaude Opus 5が担当しました。エンジニアはアーキテクチャの決定、制約の定義、変更のレビュー、結果の検証を行いました。
最初からVercelを離れようとしたわけではない
最初に考えたのは、既存の構成を最適化することでした。
着手先として最も明らかだったのはwebcatalog.ioです。一般公開サイトとして多くのアクセスを受ける一方、ほとんどのページは比較的めったに変わりません。調べてみると、動的にレンダリングされている部分が多すぎました。そこで、意図しない動的レンダリングを修正し、アクセスの多いルートに**ISR(Incremental Static Regeneration、段階的静的再生成)**を導入し、より多くのページをキャッシュ可能にして、負荷の高いサイトマップ処理を最適化しました。
これらの作業は効果がありましたが、根本的な問題もより明確になりました。比較的静的なコンテンツがなぜ動的になっているのか、どのフレームワークの挙動が原因なのか、再びキャッシュ可能にするにはどの機能を導入すべきなのかを理解するために、費やす時間が増えていたのです。
一方、ほかの多くのアプリには逆の問題がありました。アカウント設定、開発者コンソール、管理アプリ、製品インターフェースの大半には、サーバーレンダリングがまったく必要ありませんでした。APIと通信するブラウザーアプリだったのです。
その時点で、問いは「どうすればVercelの請求額を減らせるか」から、「これらがすでにNext.jsアプリでなかったとしたら、何を作るか」に変わりました。
Cloudflare上でのNext.jsも検討した
変更が最も少ない選択肢は、Next.jsを維持したままVercelからCloudflare Workersへ移すことでした。
アダプターを使えばNext.jsはCloudflare上で動作し、既存のアプリ構造の多くを維持できるため、真剣に検討しました。
ただし、Cloudflare上のNext.jsがVercel上のNext.jsと同等だとは考えませんでした。Next.jsはVercelのランタイム、デプロイシステム、キャッシュの挙動、プラットフォーム機能と最も緊密に統合されています。Cloudflare上では、アダプターがそれらの前提を別のランタイム向けに変換する必要があります。
それでもうまく動く場合はあります。しかし、技術スタックをシンプルにすることが目的なら、互換性のための層をもう一つ追加するのは理想的ではありませんでした。
ツール面にも、より広い問題がありました。デスクトップアプリ、ブラウザー拡張機能、SPAなど、私たちがフロントエンドで開発しているほぼすべてのものが、すでにViteを使っていました。
Next.jsはTurbopackへ大きく舵を切っており、Turbopackも大幅に改善されています。これは性能に対する不満ではありません。私たちにとって大きな違いはエコシステムでした。Viteはすでにフロントエンドのコードベースの大部分で共通の基盤となっており、プラグインのエコシステムがより幅広く、設定もプロジェクト間で再利用しやすかったのです。
そのため、Next.jsをCloudflare上に維持すればホスティングの問題の一部は解決できても、フレームワークの不一致と、別個のビルドツールのエコシステムは残ります。
そこで一歩引いて、各ワークロードに実際に何が必要かを考えました。
ワークロードごとに技術スタックを分けた
Next.jsに代わる万能の選択肢を探すのをやめると、アーキテクチャはずっとシンプルになりました。
マーケティングサイトには、一般公開される多言語対応ページ、メタデータ、正規URL、サイトマップ、検索エンジン経由のトラフィックがあるため、SSRが必要でした。
それ以外のWebアプリの大半にはSSRが不要で、TanStack Routerを使うViteアプリで十分でした。
APIにはReactフレームワーク自体が不要で、通常のNode.jsランタイムのほうが適していました。
最終的な構成は次のとおりです。
Webアプリ
Vite + TanStack Router
│
└── Cloudflare Workers
マーケティングサイト
TanStack Start + SSR
│
└── Cloudflare Workers
API
Hono + tRPC
│
└── Railway / Node.jsコンテナ
方針は明快になりました。SSRに実際の価値があるところではSSRを使い、そうでないところではシンプルなSPAを使い、バックエンドサービスはバックエンド向けのランタイムで動かす。
Webアプリの大半をSPAにした
SPAへの移行は最も簡単な部分でした。
WebCatalogのWebアプリは特に速く移行できました。Vite + TanStack Routerによる新しいアプリのひな型を作り、インターフェースを移植し、デプロイ先をCloudflare Workersに切り替え、古いNext.js版を削除するまで、同じ日の午前中に完了しました。
LexibirdのWebアプリ、アカウント設定、開発者コンソール、認証、管理アプリにも同じ方法を繰り返しました。最初のSPAのひな型を作ってから最後のNext.jsアプリを削除するまで、約19日でした。
これらのアプリでは、Cloudflare Workersの役割は意図的に単純にしています。Viteが静的アセットをビルドし、Cloudflareがそれを配信します。アプリのルートはindex.htmlにフォールバックします。サーバーレンダリングが必要なものはないので、SSRランタイムもありません。
難しかったのは、長年にわたってアプリの周辺に積み重なった挙動を維持することでした。認証ロジックの一部はNext.jsのサーバールートからAPIへ移す必要があり、WebCatalogの古いデスクトップアプリが依存していた旧エンドポイントも動作させ続ける必要がありました。
ReactのUIを移すのは簡単でした。
契約を守るほうが難しかったのです。
Astroを試し、5日後に削除した
マーケティングサイトはもっと難しいものでした。
最初に選んだのはAstroです。webcatalog.ioとlexibird.comはどちらもコンテンツの多い一般公開サイトで、AstroはCloudflareとの統合も良好なため、自然な選択に見えました。
両サイトの移植を始めましたが、5日後に中止しました。
致命的な欠点が一つあったわけではありません。小さな食い違いが次々と積み重なったのです。一部のルートはプリレンダリング時にデータベースへのアクセスが必要でしたが、私たちのビルド環境には意図的に本番データベースの認証情報を置いていませんでした。Reactアイランドを使うと、アプリ全体で状態を共有することも不自然になりました。プリレンダリングされたルートと実行時にレンダリングされるルートの違いや、リポジトリ内のツールとの相性にも苦労しました。
どの問題も解決不可能ではありませんでした。だからこそ、続けることが危険だったのです。一つずつ問題を直すうちに、さらに数週間を費やすことも十分あり得ました。
そこで実装を削除し、やり直しました。
AIエージェントによって、この判断の損得は変わっていました。機械的な移植作業の多くには、エンジニアの作業時間を何週間も費やしていませんでした。そのため、埋没費用が小さく、別のアーキテクチャのほうがよいと認めやすかったのです。
TanStack Startのほうが合っていた
マーケティングサイトの移行は、元のNext.jsコードからTanStack Startを使ってやり直しました。
こちらのほうがはるかに自然に適合しました。SPAではすでにTanStack Routerを使っていたため、ルーティング、ローダー、検索パラメーター、ナビゲーションに共通する概念を使えたからです。コードは通常のReactとTypeScriptのままで、ビルドシステムにはViteを使えました。
lexibird.comは1日でTanStack Startに移植できました。続いてwebcatalog.ioも移植しました。
TanStack Startがあらゆる問題を魔法のように解決したわけではありません。利点は、技術スタック全体が向かっている方向に合っていたことです。
モノレポでは、それが重要です。ビルドツールを共有すれば、プラグインや設定をより多く再利用でき、デバッグ時の例外が減り、開発者のコンテキストスイッチも少なくなります。コーディングエージェントが把握すべきプロジェクト固有の慣習も減ります。
私たちのトラフィックではCloudflareのほうが大幅に安かった
請求額が下がった理由はアーキテクチャだけではありません。Cloudflareの料金体系、とりわけ帯域幅に関する料金が大きな要因でした。
現在、Cloudflare Workersの有料プランは主にリクエスト数とCPU使用量に応じて課金され、Workersのデータ転送量や帯域幅については追加料金がかかりません。(developers.cloudflare.com) 一般公開サイトでは、この違いは非常に重要です。リクエストのCPU使用量がわずかでも、HTML、JavaScript、画像、フォントなどのアセットが転送されるからです。
Vercelの料金体系には、Fast Data TransferやFast Origin Transferなど、ネットワーク関連の使用量区分とその無料枠も含まれます。(vercel.com) 私たちのトラフィック特性では、Cloudflareのほうがコスト面で大幅に有利でした。
削減効果は、プロバイダーの変更だけで得られたものではありません。多くのアプリを静的なViteビルドに移し、不要なSSRを減らし、キャッシュの方針をより明示的にし、APIのワークロードをより適したランタイムに移しました。それでも、Cloudflareの料金体系、特に帯域幅に関する料金は、観測された約80%の削減に大きく貢献しました。
この数字は私たちのワークロードに固有のものです。VercelからCloudflareへ移るすべてのアプリで80%節約できるという意味ではありません。
APIはRailwayに落ち着いた
APIは別の課題でした。
Next.jsプロジェクトではありましたが、APIの大半はすでにNext.jsから独立していました。WebCatalogのAPIは約34,000行ありましたが、next/serverをインポートしているファイルは7つだけでした。tRPCはすでにFetchアダプターを使っており、InngestもHonoをサポートしていました。
そこで、Next.jsによるHTTP処理部分をHonoに置き換えました。この作業は驚くほど小規模でした。
より難しかったのは、どこで動かすかという問題です。APIではNode.jsやsharpなどのネイティブ依存関係を使うため、通常のWorkersは適していませんでした。
最初はCloudflare Containersを試し、速やかにVercelから移ることができました。しかし、本番環境で運用してみると、当時の私たちが望んでいた以上に、キャパシティーを手動で調整する必要がありました。
そこで、APIをもう一度移しました。
現在はRailway上で、通常の長時間稼働するNode.jsコンテナサービスとして動かしています。**CI(継続的インテグレーション)**でDockerイメージをビルドしてレジストリにプッシュし、Railwayが水平オートスケーリングを担います。
すべてをエッジで動かす必要はありません。
Vercelを離れると、自分たちで担うことが増えた
コストの低下にはトレードオフがありました。
VercelとNext.jsは、インフラに関する多くの挙動を高いレベルで処理してくれていました。TanStack StartとWorkersでは、代わりに明示的なHTTPキャッシュへ移行しました。ページをレンダリングし、キャッシュヘッダーを返し、Cloudflareがレスポンスをキャッシュします。ブラウザーとエッジには、エッジでのstale-while-revalidateのような挙動も含め、それぞれ異なるキャッシュ方針を適用できます。
こうした判断を明示的に行えるのは気に入りましたが、インフラを明示的に管理するなら、ミスも自分たちの責任になります。
実際にいくつかミスをしました。本番環境のあるキャッシュルールは、誤って訪問者固有のTanStackサーバー関数のレスポンスにも一致していました。また、別のキャッシュ設定はSPAのフォールバックの挙動と悪い形で干渉しました。これらのバグは、移行が正しく行われたかどうかをリポジトリだけで完全には証明できないことを思い出させてくれました。本番インフラもシステムの一部なのです。
AIエージェントが移行の進め方を変えた
実装作業の大半は、主にClaude Fable 5とClaude Opus 5というコーディングエージェントが行いました。
フレームワークの移行は、エージェントに特に向いています。作業の多くに、参照すべき既存の実装があるからです。このルートを移す、このURLを維持する、このフレームワークAPIを置き換える、同じメタデータを保つ、型チェックを実行してエラーを直す、結果を古い実装と比較する、といった具合です。
webcatalog.ioについては、基盤、製品ページ、カタログページ、検索、ブログ、料金、リダイレクト、サイトマップ、切り替えといった段階に作業を分けた移行トラッカーを管理しました。エージェントに「webcatalog.ioをTanStack Startに移行して」と頼むのではなく、守るべき条件を明示した、範囲の定まったタスクを渡しました。
既存のアプリが仕様になりました。エージェントの仕事は、新しいアーキテクチャでその挙動を再現することであり、製品を作り直すことではありませんでした。
それによって、人間の作業の重点はコードを手作業で移し替えることから、次のような問いへ移りました。このページに本当にSSRは必要か。どの挙動が意図的なものか。何を全体で共有するキャッシュに保存してよいか。どのURLを完全に維持しなければならないか。認証はどこで行うべきか。ある方法を実現しようとするのを、いつやめるべきか。
もちろん、出力はレビューし、ビルドと型チェックを実行し、認証、SEO(検索エンジン最適化)、リダイレクト、キャッシュといったリスクの高い領域は手動で検証しました。
重要な変化は、AIが私たちの代わりにコードを書いたことではありません。AIによって試行錯誤のコストが下がったことです。
Astroを試して5日後に削除する判断は、ずっと正当化しやすくなりました。APIを一度移した後、別のプラットフォームのほうが適していると判断しても、負担は小さくなりました。機械的な作業が安くなったため、投じた労力を惜しんで間違ったアーキテクチャを続けるのではなく、方向転換できたのです。
エージェントによって、実装作業は以前ほど希少なリソースではなくなりました。
その分、アーキテクチャ、制約、レビュー、検証がより重要になりました。
80%の削減が約90%になった
移行後、Webインフラのコストは約80%低下しました。
その後、そもそもどのリクエストがアプリに到達しているのか、さらに注意を払うようになりました。
公開インターネット上のトラフィックのかなりの部分は自動化されたものです。検索エンジン、AIクローラー、エージェント、スクレイパー、スキャナー、それほど友好的ではないボットなどが含まれます。有益なトラフィックもあれば、そうでないものもあります。
アプリがCloudflareのすぐ背後にある構成になったため、セキュリティルールを強化し、悪意のある自動化トラフィックがアプリの計算処理やデータベース処理を発生させる前に、エッジで拒否できるようにしました。
その結果、従来の構成と比較したコスト削減率は全体で約**90%**に達しました。
最終的な削減は、いくつもの要素が組み合わさった結果です。Cloudflareの低い料金、とりわけ帯域幅に関する料金、サーバー側の処理の削減、APIに適したランタイム、より明示的なキャッシュ、そして不要なリクエストがそもそもアプリに届かなくなったことです。
最終的な構成
私たちのリポジトリには、もうNext.jsアプリは残っていません。Webアプリの大半は現在、Cloudflare Workers上のVite + TanStack RouterによるSPAです。webcatalog.ioとlexibird.comはSSRに実際のメリットがあるため、Cloudflare Workers上のTanStack Startを使っています。APIはRailway上のHono + tRPCで、通常のNode.jsコンテナとして動作しています。フロントエンドのプロジェクトも、ほぼすべてがViteのエコシステムに収まりました。
インフラコストは約**80%下がりました。私たちのトラフィックに対してCloudflareの料金が大幅に有利だったこと、特に帯域幅に関する料金が大きく貢献しています。悪意のある自動化トラフィックをエッジでフィルタリングしたことで、全体の削減率は約90%**に達しました。
ただ、私たちが最も重視している成果はアーキテクチャです。マーケティングサイトにはSSRを使い、SPAにできるものはすべてSPAにし、実際のサーバーを使うほうがシンプルな場合はAPIをサーバー上で動かします。
最初はVercelのコストを下げようとしていました。
最終的には、料金を払って使っていたアーキテクチャの大半は必要なかったと気づいたのです。