Vercelのデフォルトは米国──3つのプロジェクトで同じ罠にハマった話

はじめに

ある日、自分が運用している塾のLMSシステムのページ遷移が妙に遅いことに気づきました。
体感で2〜3秒。
クライアント側の操作は即座に反応するのに、ページを移動するたびにもたつく。

調査を進めた結果、原因は意外なところにありました。
Vercel FunctionsがワシントンD.C.で動いていたのです。
データベースは東京にあるのに。

そして「もしかして他のプロジェクトも?」と確認したところ、案の定──運用中の3つのプロジェクトすべてが同じ状態でした。

修正は vercel.json への1行追加。
コードの変更はゼロ。
しかし効果は劇的でした。

Vercel Region Mismatch Latency

何が起きていたのか

Vercel Serverless Functionsには「デフォルトリージョン」があります。
iad1──ワシントンD.C.、米国東海岸です。

vercel.json でリージョンを明示的に指定しない限り、世界中どこからデプロイしても、関数は米国東海岸で実行されます。

一方、Supabaseのデータベースは東京リージョン(ap-northeast-1)に配置していました。
つまり、こういう構図です。

ユーザー(日本)
  → Vercel Edge(日本のCDN)→ 静的アセットは速い ✅
  → Vercel Functions(米国東部 iad1)→ Supabase(東京)
    ↑ データが太平洋を往復 ❌

東京とワシントンD.C.の距離は約10,900km。
光ファイバーでの片道遅延は約70〜90ms。
TCP+TLSハンドシェイクを含めた往復では、1リクエストあたり約150〜200msの余分なレイテンシーが発生します。

1回のページ表示で複数のDBクエリを実行すれば、この遅延は累積していきます。

3つのプロジェクトでの影響

問題が発覚したのは以下の3つのプロジェクトです。
構成はそれぞれ異なりますが、根本原因はまったく同じでした。

1. 塾LMSシステム(Next.js + Supabase)

最初に問題に気づいたプロジェクトです。
サーバーサイドの各処理に Date.now() を埋め込み、vercel logs で実測しました。

処理 修正前 修正後
middleware getUser() 300ms 110ms
resolveStudentId() 530ms 65ms
DBクエリ(Promise.all × 5) 776ms 55ms
dashboard合計 1,319ms ~230ms
成績ページ合計 1,489ms ~250ms

ページ全体で約5〜6倍の高速化
ダッシュボードの表示が1.3秒から230msになった──体感でまったく別物です。

2. ずぼらメモ(React Native + Vercel + Supabase)

iOSネイティブアプリですが、サーバーサイドはVercel上のAPIサーバー(Express + tRPC)で動いており、そこからSupabaseのデータベースにアクセスする構成です。
つまりLMSやじぶんブログと同じく、Vercel Functions → Supabase間の通信が米国を経由していました。

こちらは厳密なログ計測は行っていませんが、ネットワーク構成から推定すると以下のような改善が見込まれます。

指標 修正前(推定) 修正後(推定)
Functions → DB間 ~150ms <5ms
全体のネットワーク往復 ~310ms ~25ms

AI処理フロー(メモ保存→タグ取得→類似検索→リンク作成)のように複数のDBクエリが連続する場面では、推定で150ms × 4回 = 600msの通信が20ms程度に短縮される計算です。
実際にリージョン変更後にアプリを触ると、メモの保存や一覧取得が明らかに速くなっており、体感で別物でした。

3. じぶんブログ(Next.js + Supabase)

Server Actionを使うすべての操作──記事一覧取得、保存・更新・削除、ログイン認証、Stripe Webhook処理──に影響が出ていました。
構造的には塾LMSと同じ問題です。

Blind spot in Serverless Setup

なぜ気づけなかったのか

この問題が厄介なのは、気づきにくいことです。
理由はいくつかあります。

Vercelの「ゼロコンフィグ」思想

Vercelは「設定なしでデプロイできる」ことを売りにしています。
実際、vercel.json がなくてもデプロイは成功します。
レスポンスも返ってきます。
ただ遅いだけ。

デプロイが「成功」するため、問題に気づきにくいのです。

開発環境では再現しない

開発中は localhost でDBに直接アクセスするため、デプロイ後の遅延に気づきません。
「本番でなんとなく遅い気がするけど、まあサーバーレスだから……」で見過ごしてしまいます。

CDNが「速い」と錯覚させる

静的アセットは東京のEdgeから配信されるので、ページの表示自体は速く感じます。
遅いのはデータ取得部分のみ。
体感では「なんとなくもっさり」程度で、バグとは認識しにくい。

Supabaseのリージョン選択が安心感を与える

Supabaseのプロジェクト作成時には、リージョン選択のUIが明示的に存在します。
東京を選べば「よし、日本に近い場所にデータを置いた」と安心する。

しかし、Vercel FunctionsとSupabaseは独立したサービスです。
Supabaseが東京にあっても、VercelがUS Eastを使い続けるという事実は、ドキュメントを注意深く読まない限り気づけません。

ネイティブアプリ開発者の盲点

Webアプリ開発では、CDNやサーバーリージョンは当然のように議論されます。
しかしネイティブアプリ開発では、「APIのURLを設定すれば動く」で終わりがち。
サーバーの物理的な所在地まで気が回りにくいのです。

One-line fix in vercel.json

修正:たった1行

修正は驚くほど簡単です。
vercel.json に1行追加するだけ。

{
  "regions": ["hnd1"]
}

hnd1 はVercelの東京リージョン。
hnd は羽田空港のIATAコードで、Vercelはデータセンター近くの空港コードをリージョン識別子として使っています。

これだけで、全Serverless Functionが東京で実行されるようになります。
次回デプロイから自動適用。
コードの変更は一切なし。

修正後の構成

ユーザー(日本)
  → Vercel Edge(日本のCDN)→ 静的アセット ✅
  → Vercel Functions(東京 hnd1)→ Supabase(東京)
    ↑ 同一リージョン内通信(数ms) ✅

診断方法

自分のプロジェクトが同じ問題を抱えていないか、簡単に確認できます。

x-vercel-id ヘッダーを見る

curl -sI https://your-app.vercel.app/api/endpoint | grep x-vercel-id

出力の形式は {edge-region}::{function-region}::{request-id}
2番目の部分が関数の実行リージョンです。
ここが iad1 になっていたら、関数は米国で動いています。

vercel inspect コマンド

vercel inspect <deployment-url>

ビルド出力に [iad1] と表示されていれば、デプロイ先は米国東海岸です。

vercel logs でリアルタイム計測

vercel logs <domain>

サーバーサイドに Date.now() を埋め込み、このコマンドでリアルタイムにFunction Logsを確認できます。
ダッシュボードを開かずにターミナルから計測可能です。

最適化は順番が大切

ずぼらメモでは、リージョン修正の1週間前にJWT検証のローカル化を実施していました。
それまでは認証のたびにSupabase Auth APIへのHTTPリクエストが発生しており、1リクエストあたり2回のSupabase通信が起きていたのを、joseライブラリによるローカル検証で1回に減らしました。

この最適化を先に行ったことで、リージョン問題の影響がDB往復のみに絞られ、問題の切り分けが容易になりました。

塾LMSでも同様に、React.cache() による認証クエリの重複排除や、Link コンポーネントのprefetch制御を実施しましたが、最も効果が大きかったのはリージョンの修正でした。

パフォーマンス改善では、コードの最適化よりもインフラの最適化の方が桁違いの効果を生むことがあります。
「React.cacheで速くなるはず」という推測よりも、console.log での実測が根本原因の発見につながりました。

再発防止

同じ問題を二度と起こさないために、Claude Codeのスキルを作成しました。
Vercel関連の設定作業を行う際に、AIが自動的に regions: ["hnd1"] の設定を提案してくれます。

人間のチェックリストと似た仕組みですが、AIが自律的に適用する点が異なります。
新規プロジェクトでも初回セットアップ時に適用されるため、設定の抜け漏れを防止できます。

この問題が当てはまる構成

この問題は特定のプロジェクトに限った話ではありません。
以下のいずれかに該当するなら、確認する価値があります。

  • Vercel + Supabase(最も典型的なパターン)
  • Vercel + PlanetScale / Neon / Turso
  • Vercel + AWSの東京リージョンリソース(DynamoDB, RDS等)
  • Vercel + 地域限定の外部API

共通する根本問題は、Vercelのデフォルトリージョンが米国東海岸であることを認識していないと、バックエンドのリージョン選択がどれだけ適切でも、余分なレイテンシーが加算されるということです。

Vercelの主要リージョン

コード 場所 備考
iad1 ワシントンD.C. デフォルト
hnd1 東京 日本向けはこれ
icn1 ソウル
sin1 シンガポール
sfo1 サンフランシスコ
cdg1 パリ
syd1 シドニー

Hobbyプランでは1リージョンのみ指定可能。
Proプラン以上で複数リージョンの指定ができます。

まとめ

今回の学びは5つに集約されます。

  1. 「デフォルト」を信じるな──グローバルなクラウドサービスのデフォルトは米国基準。「何も設定しなくても動く」は「最適な状態で動く」とは違う
  2. 推測より計測──「サーバーレスだから仕方ない」「コールドスタートだろう」と推測で片付けず、実際に計測すれば根本原因が見える
  3. インフラのリージョンは揃える──DB・Functions・その他バックエンドサービスは同一リージョンに配置する
  4. 小さな設定が大きな効果を生む──1行の設定追加でレイテンシー92%削減。コード最適化よりインフラ最適化が桁違いに効くことがある
  5. 気づいたら仕組み化する──同じミスを繰り返さないよう、チェックリストやスキルとして定着させる

もしVercel + Supabase(または他のDB)の構成で「なんとなく遅い」と感じているなら、まず curl -sIx-vercel-id ヘッダーを確認してみてください。
答えはそこにあるかもしれません。