はじめに
Vercelは「Next.jsをデプロイする場所」という印象が強い。
ボタンを押すだけでデプロイできるし、GitHubと連携すればPreview環境も勝手に作られる。フロントエンドエンジニアにとってはかなり便利なホスティング基盤だ。
ただ、少し真面目に運用しようとすると、Vercelは単なるホスティングサービスではなくなる。
特に次のあたりは、かなりインフラ寄りの話になる。
- CDN Cache
- Functionの実行リージョン
- ISRのキャッシュリージョン
-
Cache-Controlの扱い -
x-vercel-cacheによるキャッシュ確認 -
x-vercel-idによるリージョン確認 - Build Output API
- Fluid Compute
この記事では、Vercelを「フロントエンドのデプロイ先」ではなく、「CDNと実行基盤を持つアプリケーションインフラ」として見ていく。
かなりニッチだが、Vercelで本番運用するなら見ておきたい観点だ。
Vercelを雑に理解すると事故る
Vercelを雑に見ると、次のような理解になりやすい。
ユーザー
↓
Vercel
↓
Next.js
↓
DB / API
しかし、インフラ目線ではこれだと粗すぎる。
もう少し分解すると、少なくとも次のように見る必要がある。
ユーザー
↓
Vercel Edge Network
↓
CDN Cache
↓
Routing / Middleware
↓
Vercel Functions
↓
外部API / DB / Storage
この中で重要なのは、キャッシュされる場所 と コードが実行される場所 が同じではないという点だ。
「Vercelに置いたから世界中で速い」と考えるのは危ない。
静的アセットやキャッシュ済みレスポンスはユーザーに近い場所から返せる。一方で、FunctionがDBへアクセスする場合、Functionの実行リージョンとDBの距離がそのままレイテンシに効く。
つまり、Vercelの性能を見るときは、次の2つを分けて考える必要がある。
- キャッシュヒット時の速さ
- Function実行時の速さ
この2つは別物だ。
Vercelの「速さ」はCDNだけでは決まらない
VercelのCDN Cacheは、ページ、APIレスポンス、静的アセットなどを世界中の拠点にキャッシュできる。
静的アセットならイメージしやすい。ハッシュ付きのJSやCSS、画像などは、デプロイごとの成果物としてCDNから配信される。
問題は、動的レスポンスだ。
たとえばAPI RouteやRoute Handlerで次のようなレスポンスを返すとする。
export async function GET() {
const data = await fetchSomething();
return Response.json(data, {
headers: {
"Cache-Control": "s-maxage=60, stale-while-revalidate=300",
},
});
}
この場合、レスポンスはVercelのCDNにキャッシュされる可能性がある。
ここで見るべきなのは、ブラウザキャッシュではなく、CDN側のキャッシュだ。
よくある勘違いは、Cache-Control を見るだけで「ブラウザにも同じように返っている」と思い込むことだ。VercelではCDN向けのキャッシュ制御とブラウザ向けのキャッシュ制御を分けられる。
たとえば次のように書ける。
export async function GET() {
return new Response("hello", {
headers: {
"Cache-Control": "max-age=10",
"CDN-Cache-Control": "max-age=60",
"Vercel-CDN-Cache-Control": "max-age=3600",
},
});
}
これはかなりインフラっぽい。
- ブラウザには短くキャッシュさせる
- 一般的なCDNには少し長くキャッシュさせる
- Vercel CDNにはさらに長くキャッシュさせる
という分離ができるからだ。
Vercelを単なるNext.jsのデプロイ先として見ていると、このあたりを見落としやすい。
x-vercel-cache を見る
キャッシュが効いているかどうかは、レスポンスヘッダーで確認できる。
curl -I https://example.com/api/sample
見るべきヘッダーはこれだ。
x-vercel-cache
値は状況によって変わるが、キャッシュの状態を見る手がかりになる。
たとえば、キャッシュが効いていないなら、Functionが毎回実行されている可能性がある。逆にキャッシュヒットしているなら、Functionまで到達せずにCDNから返っている可能性がある。
本番障害の調査では、ここが地味に重要になる。
「APIが遅い」と言われたときに、本当にFunctionが遅いのか、キャッシュが効いていないのか、特定のリージョンだけキャッシュが冷えているのかを切り分ける必要がある。
その第一歩が x-vercel-cache の確認だ。
x-vercel-id はもっと地味だが重要
もう1つ見たいヘッダーがある。
x-vercel-id
これは、リクエストが通ったVercelのリージョンや、Functionが実行されたリージョンを確認するための手がかりになる。
curl -I https://example.com/api/sample
レスポンスに x-vercel-id が含まれていれば、どのリージョンを経由したかを調査できる。
これは、レイテンシ調査で効いてくる。
たとえば日本のユーザー向けサービスなのに、Functionが米国東部で動いていて、そこから東京リージョンのDBへアクセスしているとする。
この場合、ユーザーから見ると次のような経路になる可能性がある。
日本のユーザー
↓
近いEdge
↓
米国東部のFunction
↓
東京のDB
これはなかなかつらい。
Vercelのエッジで受けているから速いように見えても、実際の動的処理が遠いリージョンで動いていれば、DBアクセスで遅くなる。
だから、Vercelでは「エッジに乗っているか」だけではなく、「Functionがどこで動いているか」を確認する必要がある。
FunctionリージョンはDBに寄せる
Vercel Functionsは実行リージョンを指定できる。
たとえば、vercel.json で次のように指定する。
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"regions": ["hnd1"]
}
日本向けで、DBも東京リージョンにあるなら、Functionを東京に寄せる判断は自然だ。
ただし、ここで注意がある。
ユーザーに近づけるのではなく、データソースに近づける という考え方が重要だ。
FunctionがDBや外部APIにアクセスするなら、FunctionとDBの距離が支配的になることが多い。
たとえば、ユーザーが日本にいて、DBも東京なら東京に寄せる。
日本のユーザー
↓
Vercel Edge
↓
東京のFunction
↓
東京のDB
これは分かりやすい。
一方で、ユーザーは日本だが、DBが米国東部にあるなら話が変わる。
日本のユーザー
↓
Vercel Edge
↓
東京のFunction
↓
米国東部のDB
この場合、Functionを東京に置くと、FunctionからDBへの往復が重くなる。
だったら、FunctionをDBに近い米国東部に置いた方が速いケースもある。
日本のユーザー
↓
Vercel Edge
↓
米国東部のFunction
↓
米国東部のDB
つまり、Vercelのリージョン設計では「ユーザーに近いから正義」とは限らない。
Functionは、ユーザーよりもDBや外部APIに寄せた方がよいことがある。
このあたりは、普通のクラウド設計と同じだ。
ISRのキャッシュもリージョンを意識する
ISRも地味にインフラ要素が強い。
ISRは、ビルドし直さずにページを再生成できる仕組みだ。Next.jsではかなり便利な機能だが、Vercel上ではキャッシュリージョンの話が出てくる。
特に見落としやすいのは、ISRのキャッシュがプロジェクトのデフォルトFunctionリージョンに関係する点だ。
つまり、ISRを使う場合も、Functionリージョンの設定は単なる動的APIの設定ではなく、キャッシュや再生成の挙動にも影響する。
「静的ページだからリージョンは関係ない」と思うと、少し危ない。
再生成時にはデータを取りに行く。そこでDBやCMSが遠ければ、再生成が遅くなる。
キャッシュ済みページ
→ 速い
再生成
→ Function実行
→ DB / CMSアクセス
→ キャッシュ更新
ISRは静的配信と動的実行の中間にある。
だからこそ、インフラ目線では「どこで再生成されるか」を見ておく必要がある。
Vercelでありがちなレイテンシ事故
Vercelで起こりがちなレイテンシ事故を、少し極端に書く。
ユーザー: 日本
Vercel Function: iad1
DB: ap-northeast-1
外部API: ap-northeast-1
この状態だと、画面はVercelのCDNから速く返るかもしれない。
しかし、ログイン後のAPIやSSRが絡むページでは、Functionが米国東部で動き、東京のDBにアクセスする。
日本
↓
米国東部
↓
東京
↓
米国東部
↓
日本
これでは遅くなって当然だ。
Vercelではデプロイ体験が簡単すぎるので、こういうネットワーク経路を忘れやすい。
しかし、やっていることは普通に分散システムだ。
DBがどこにあるか、外部APIがどこにあるか、Functionがどこで動いているかを見ないといけない。
キャッシュ設計で見るべきこと
Vercelのキャッシュ設計では、少なくとも次の観点を分けて見る必要がある。
| 観点 | 見るもの |
|---|---|
| 静的アセット | ハッシュ付きファイル、画像、CSS、JS |
| CDNキャッシュ |
s-maxage、stale-while-revalidate
|
| ブラウザキャッシュ | max-age |
| Vercel専用キャッシュ | Vercel-CDN-Cache-Control |
| キャッシュ確認 | x-vercel-cache |
| リージョン確認 | x-vercel-id |
| 再生成 | ISR、revalidate |
| 動的処理 | Functionの実行リージョン |
この表だけでも、Vercelが単なる「フロントの置き場」ではないことが分かる。
むしろ、かなり抽象化されたインフラ基盤だ。
Build Output APIが見せるVercelの本体
さらにニッチな話をすると、VercelにはBuild Output APIがある。
これは、フレームワークがVercel向けのデプロイ成果物を出力するためのファイルシステムベースの仕様だ。
普段Next.jsだけを使っていると、Build Output APIを直接触ることは少ない。
しかし、これを見ると、Vercelが何を「デプロイ可能な単位」として扱っているかが見えてくる。
ざっくり言えば、Vercelは次のような要素を成果物として扱う。
- 静的ファイル
- Functions
- Routing
- キャッシュ設定
- 設定ファイル
つまり、Vercelにおけるデプロイとは、単にソースコードを置くことではない。
ビルド結果として、Vercelが理解できるインフラ構成を吐き出すことだ。
ここが面白い。
Next.jsをVercelにデプロイしているとき、実際にはNext.jsがVercel向けの実行単位やキャッシュ単位に変換されている。
これは、フレームワークがインフラを生成しているとも言える。
Fluid Computeは「サーバーレスっぽいが、少しサーバー寄り」な実行基盤
VercelのFluid Computeも、かなりインフラ寄りの概念だ。
従来のサーバーレスは、リクエストごとに独立した実行環境を強く意識する。分離が分かりやすい一方で、コールドスタートやリソース効率の課題がある。
Fluid Computeでは、複数のリクエストが同じFunctionインスタンスを共有できる。
これは、I/O待ちが多い処理では効きやすい。
たとえば次のような処理だ。
- 外部APIを呼ぶ
- LLM APIを呼ぶ
- ベクトルDBを呼ぶ
- 通知を送る
- ログや分析イベントを送る
このような処理では、CPUをずっと使っているというより、外部サービスの応答を待っている時間が多い。
そのため、同じインスタンスで複数リクエストを扱えると、リソース効率が上がる可能性がある。
ただし、これは良いことばかりではない。
同じ実行環境を共有するということは、グローバル状態の扱いにより注意が必要になる。
たとえば、次のようなコードは危ない。
let currentUserId: string | null = null;
export async function GET(request: Request) {
currentUserId = request.headers.get("x-user-id");
return Response.json({
userId: currentUserId,
});
}
このようにリクエスト固有の値をグローバル変数に置くと、並行実行時に意図しない混線が起こり得る。
サーバーレスだから安全、という雑な理解は通用しない。
Fluid Computeを使うなら、次の原則を守るべきだ。
- リクエスト固有の状態をグローバルに置かない
- DBコネクションやSDKクライアントなど、共有してよいものだけをグローバルに置く
- キャッシュする値のスコープを明確にする
- 非同期処理の完了タイミングを意識する
- ログにリクエストIDを入れる
ここは、Node.jsのサーバーアプリケーションに近い注意点だ。
Vercelを本番で使うときのチェックリスト
Vercelを本番のインフラとして見るなら、最低限このあたりは確認したい。
リージョン
- Functionのデフォルトリージョンを確認したか
- DBや外部APIに近いリージョンになっているか
- 日本向けなのに無意識に
iad1のままになっていないか - Preview環境とProduction環境で差がないか
キャッシュ
- 静的アセットと動的レスポンスを分けて考えているか
-
Cache-Controlの意図を説明できるか - ブラウザキャッシュとCDNキャッシュを分けているか
-
x-vercel-cacheを確認しているか - キャッシュしてはいけないレスポンスをキャッシュしていないか
ISR
- ISRの再生成時にどのデータソースへアクセスするか
- 再生成が遅くなったときの影響範囲はどこか
- CMSやDBのリージョンとFunctionリージョンが離れていないか
- revalidateの設計が運用要件に合っているか
セキュリティ
- ユーザー別レスポンスをCDNにキャッシュしていないか
- CookieやAuthorization付きのレスポンスを不用意に共有キャッシュしていないか
- Preview環境のURLを外部共有してよいか
- 環境変数のスコープが適切か
運用
-
x-vercel-idでリージョンを追えるか - ログにリクエストIDやユーザー影響範囲を残しているか
- 障害時にキャッシュヒットとFunction実行を切り分けられるか
- ロールバック時にキャッシュの影響を考えているか
まとめ
Vercelは便利だ。
GitHubにpushするだけでデプロイできるし、Preview環境も自動で作られる。Next.jsとの相性もよい。
しかし、本番運用の目線で見ると、Vercelは単なるフロントエンドホスティングではない。
実態は、CDN、キャッシュ、ルーティング、Function実行基盤、ビルド成果物仕様を持つアプリケーションインフラだ。
特に重要なのは、次の3つだ。
- キャッシュされる場所
- Functionが実行される場所
- データソースが存在する場所
この3つがずれていると、見た目はモダンでも、普通に遅いシステムになる。
Vercelは簡単に使える。
だが、簡単に使えることと、何も考えなくてよいことは違う。
Vercelを本番で使うなら、x-vercel-cache と x-vercel-id を見ながら、キャッシュとリージョンをインフラとして設計する必要がある。