読み込み中...
この記事は、ひとりでつくるSaaS - 設計・実装・運用の記録 Advent Calendar 2025 の13日目の記事です。
昨日の記事では「Route HandlerからHonoへの移行」について書きました。この記事では、ビルド時間短縮とVercelでのレスポンス改善のために実践した最適化について解説します。
個人開発でNext.jsアプリをVercelにデプロイしていると、いくつかの課題が見えてきます。
この記事では、これらの課題に対して実践した最適化を紹介します。
ローカル開発では、パッケージマネージャをnpmからBunに移行しました。
# Before
npm install # 数十秒〜数分
# After
bun install # 数秒Bunはnpmと互換性がありながら、インストール速度が大幅に高速です。依存パッケージが多いプロジェクトほど効果を実感できます。
移行は簡単で、bun installを実行するだけでbun.lockが生成されます。既存のpackage.jsonはそのまま使えます。
# 移行手順
bun install
rm package-lock.json # 不要になったら削除Vercelでもvercel.jsonのinstallCommandをbun installに変更すれば使えますが、--legacy-peer-depsが必要な依存関係があるため、互換性を考慮してnpmを使っています。ローカル開発の効率は大幅に向上しました。
next.config.tsのoptimizePackageImportsで、大型ライブラリのtree-shakingを改善できます。
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
optimizePackageImports: [
'lucide-react',
'@radix-ui/react-icons',
'@radix-ui/react-dialog',
'@radix-ui/react-dropdown-menu',
'@tiptap/react',
'echarts',
'framer-motion',
'date-fns',
'recharts',
],
},
};これらのライブラリは、全体をインポートするとバンドルサイズが大きくなりがちです。この設定で、使用している部分だけがバンドルに含まれるようになります。
tsconfig.jsonでインクリメンタルビルドを有効にすると、変更がないファイルの再コンパイルをスキップできます。
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".next/cache/tsconfig.tsbuildinfo",
"skipLibCheck": true
}
}incremental: true: 増分ビルドを有効化tsBuildInfoFile: ビルド情報のキャッシュ先を指定skipLibCheck: node_modules内の型チェックをスキップリージョン設定は、最も効果を実感しやすい最適化です。有識者にとっては当たり前の設定かもしれませんが、初心者は見落としがちなポイントです。実際に開発中に体験したエピソードを紹介します。
ダッシュボード画面の読み込みが、開発環境では2秒程度なのに、本番環境では5秒以上かかっていました。
原因を調べたところ、Vercel FunctionsがデフォルトでワシントンDC(iad1)で実行されていました。データベースは東京(Supabase ap-northeast-1)にあるため、毎回太平洋を往復していたのです。
開発環境(ローカル):
ローカルPC(日本) → Supabase DB(東京) = 速い
本番環境(修正前):
Vercel Functions(ワシントンDC) → Supabase DB(東京) = 遅い
vercel.jsonにリージョン設定を追加するだけで解決しました。
{
"regions": ["hnd1"]
}hnd1は東京リージョンを指します。この1行を追加してデプロイしたところ、ダッシュボードの読み込みが5秒から2秒以下に改善されました。
実際にどのリージョンで実行されているかは、レスポンスヘッダーのx-vercel-idで確認できます。
修正前: hnd1::iad1::xxxxx
修正後: hnd1::hnd1::xxxxx
x-vercel-idの読み方は以下の通りです。
Vercelには「エッジ」と「Functions」という2種類の実行環境があります。
エッジ(Edge Network):
Functions(Serverless Functions):
リクエストの流れはこうなります。
ユーザー(日本)
↓
エッジ(東京)← 静的ファイルはここで返す
↓
Functions(東京)← API呼び出し、DB接続
↓
Supabase DB(東京)
DBと同じリージョンにFunctionsを配置することで、遅延を最小化できます。
next.config.tsのheaders()で、リソースの種類ごとにキャッシュを設定します。リソースの性質に応じて適切なキャッシュ戦略を選ぶことが重要です。
静的アセット(/_next/static/):
{
source: '/_next/static/(.*)',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' },
],
}Next.jsの静的アセットはファイル名にハッシュが含まれるため、内容が変わればURLも変わります。古いキャッシュが問題になることがないため、1年間の長期キャッシュが可能です。
HTMLページ:
{
source: '/(.*)',
headers: [
{ key: 'Cache-Control', value: 'public, max-age=0, must-revalidate' },
],
}HTMLは動的に変わる可能性があるため、毎回サーバーに確認します。ただし、変更がなければ304レスポンスで効率的に処理されます。
API:
{
source: '/api/(.*)',
headers: [
{ key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' },
],
}APIは認証情報やユーザー固有のデータを返すことがあるため、キャッシュを完全に無効化しています。古いデータが返されると不整合が発生するリスクがあります。
vercel.jsonで、処理時間がかかるAPIのタイムアウトを個別に設定できます。
{
"functions": {
"src/app/api/search/route.ts": { "maxDuration": 30 },
"src/app/api/chat/route.ts": { "maxDuration": 60 },
"src/app/api/embeddings/route.ts": { "maxDuration": 30 }
}
}Hobbyプランのデフォルトタイムアウトは10秒ですが、LLMを使った処理やベクトル検索など時間がかかるAPIは個別に延長します。
export const runtime = 'edge'で軽量な処理をエッジで実行(OG画像生成など)next/fontで必要なサブセット・ウェイトのみを読み込みmatcher設定で、静的ファイルやAPIルートはMiddlewareをスキップnext/imageでWebP変換・リサイズを自動化これらの最適化を適用した結果をまとめます。
| 項目 | Before | After |
|---|---|---|
| ローカルインストール | npm(数十秒) | Bun(数秒) |
| リージョン | iad1(ワシントンDC) | hnd1(東京) |
| ダッシュボード表示 | 5秒以上 | 2秒以下 |
| 静的アセット | 毎回取得 | 1年間キャッシュ |
特にリージョン設定は、1行の変更で体感できるレベルの改善が得られました。
Vercelでのパフォーマンス最適化について解説しました。
ビルド時間短縮:
optimizePackageImportsで大型ライブラリを最適化レスポンス改善:
個人開発では、最初から完璧な最適化は不要です。ユーザーからのフィードバックやVercel Analyticsを見ながら、必要な箇所から改善していくのがおすすめです。
明日は「モバイルファーストで最適なUXを考える」について解説します。
シリーズの他の記事
コメント