Skip to content

Vercelデプロイ最適化:Next.js本番運用でハマるポイント総まとめ

Next.jsとVercelは、モダンなウェブアプリケーション開発において強力な組み合わせを提供します。しかし、開発環境で問題なく動作したアプリケーションが、Vercel上での本番デプロイ時に予期せぬ挙動を示したり、パフォーマンス上の課題に直面したりすることは少なくありません。本記事では、Next.js App Routerで開発されたアプリケーションをVercelへ本番デプロイする際に中級Webエンジニアが陥りやすいポイントを特定し、その回避策と最適化パターンを2026年時点のベストプラクティスとして解説します。

1. 導入

Next.jsのApp Routerは、コンポーネント指向のアプローチとサーバーコンポーネントの導入により、開発体験とアプリケーションのパフォーマンスを大きく向上させました。VercelはそのNext.jsを最も効果的にデプロイできるプラットフォームとして広く利用されています。開発の容易さとは裏腹に、本番環境での安定稼働や最適化には、ローカル環境との差異、キャッシング戦略、ランタイムの特性など、多岐にわたる考慮が必要です。本記事では、これらの「ハマりどころ」を事前に理解し、堅牢で高性能なアプリケーションを構築するための知識を提供します。

2. なぜローカルとVercel本番で差が出るのか

ローカル環境とVercelの本番環境では、アプリケーションの実行コンテキストが根本的に異なります。ローカルでは通常、単一のNode.jsプロセスで開発サーバーが動作しますが、Vercelではアプリケーションはビルド成果物として変換され、Vercelのグローバルエッジネットワーク上に分散されたサーバーレス関数や静的ファイルとしてデプロイされます。

この違いにより、以下のような状況が発生します。

  • ビルドプロセスの最適化: Vercelはnext buildコマンドを実行し、アプリケーションを静的アセット、サーバーレス関数(API RoutesやSSRページ)、エッジ関数(MiddlewareやEdge Runtime)に最適化して変換します。このプロセスで、ローカルでは見過ごされがちな依存関係の問題や、ビルド時の環境変数の欠落が露呈することがあります。
  • 分散環境: VercelはCDN(Contents Delivery Network)とサーバーレス関数を利用した分散アーキテクチャです。ユーザーからのリクエストは地理的に最も近いエッジロケーションで処理されるため、ローカルでの単一サーバーの挙動とは異なるレイテンシやキャッシュヒット率の挙動を示します。
  • ランタイム環境の差異: Vercelのサーバーレス関数やエッジ関数は、実行環境の制約(メモリ、CPU、実行時間など)や利用可能なグローバルオブジェクトがローカルのNode.js環境と完全に同一ではありません。

これらの差異を理解することが、本番環境での問題解決の第一歩となります。

3. 環境変数・Build Output・Node/Edge Runtimeの整理

環境変数

環境変数は、ローカルと本番環境で異なる設定を管理するための重要な手段です。Vercelでは環境変数の扱い方に特有のルールがあります。

  • Public (公開) 環境変数: NEXT_PUBLIC_ プレフィックスを持つ変数は、ブラウザ側(クライアントサイド)のJavaScriptバンドルにも含まれます。APIキーなど機密性の高い情報は決して含めないでください。
  • Private (秘密) 環境変数: プレフィックスを持たない、または NEXT_PUBLIC_ 以外で始まる変数は、サーバーサイドコードでのみ利用可能です。これらはビルド時または実行時にサーバーレス関数に安全に渡されます。
  • Vercelダッシュボードでの設定: VercelプロジェクトのSettings > Environment Variablesから設定します。ここで設定された変数は、ローカルの.envファイルよりも優先されます。
  • ビルド時と実行時: 環境変数はビルド時に埋め込まれるものと、サーバーレス関数が実行されるたびに提供されるものがあります。ビルド時に利用される変数は、ビルド後に変更しても反映されません。ランタイム時に利用される変数は、デプロイ後にVercelダッシュボードで変更し、再デプロイなしに反映させることができます(ただし、Next.jsのキャッシュ戦略によっては再ビルドが必要な場合もあります)。

Build Output

next buildコマンドを実行すると、.nextディレクトリに最適化されたビルド成果物が生成されます。Vercelはこのディレクトリをデプロイし、以下のような要素に分割して実行します。

  • 静的アセット: publicディレクトリ内のファイルや、ビルド時に生成されるJavaScript、CSSなどのアセットはVercelのCDNに配置されます。
  • サーバーレス関数: App RouterのPageやLayout、Route Handlersの一部はNode.jsサーバーレス関数としてデプロイされます。これらは必要に応じて起動し、リクエストを処理します。
  • エッジ関数: Middlewareや特定のRoute Handlersは、Edge Runtime上で動作するエッジ関数としてデプロイされます。

Node.js RuntimeとEdge Runtime

Next.jsでは、サーバーサイドのコードを実行するランタイムとしてNode.js RuntimeとEdge Runtimeの2種類を選択できます。

  • Node.js Runtime:
    • 従来のNode.js環境で、フル機能のNode.js APIが利用可能です。
    • 大規模な計算、ファイルシステムアクセス、データベース接続ライブラリなど、豊富なエコシステムが利用できます。
    • デメリットとして、コールドスタート(アイドル状態から初めてリクエストを処理するまでの起動時間)がEdge Runtimeよりも長くなる傾向があります。
    • 設定例:
      ts
      // app/api/heavy-computation/route.ts
      export const runtime = 'nodejs';
      
      export async function GET() {
        // データベース操作や重い処理
        return new Response('Node.js Runtime response');
      }
  • Edge Runtime:
    • V8 JavaScriptエンジン上で動作し、より軽量で高速な起動が可能です。
    • CDNのエッジロケーションで実行されるため、ユーザーに近い場所で応答でき、低レイテンシを実現します。
    • 利用可能なWeb APIが限定されており、Node.jsの組み込みモジュールやファイルシステムアクセスなど一部の機能は利用できません。ネットワークI/Oに最適化されています。
    • Middlewareは常にEdge Runtimeで実行されます。
    • 設定例:
      ts
      // app/api/geo-data/route.ts
      export const runtime = 'edge'; // 明示的に指定
      
      export async function GET(request: Request) {
        // リクエストヘッダーから地理情報に基づいてデータを返す
        const country = request.headers.get('x-vercel-ip-country');
        return new Response(`You are in ${country || 'Unknown'}`);
      }
    Edge Runtimeは、認証、リダイレクト、A/Bテスト、地理ベースのコンテンツ配信など、軽量で高速な処理が求められるケースに最適です。重い処理や多くのメモリを必要とする処理にはNode.js Runtimeを選択するなど、用途に応じて使い分けることが重要です。

4. キャッシュとISR/SSG/SSRの設計でハマる点

Vercel上でNext.jsを運用する際、最も複雑でハマりやすいのがキャッシュ戦略です。App Routerではfetch APIの拡張、export const revalidatedynamicオプションを組み合わせてキャッシュ挙動を制御します。

fetch キャッシュ指定の違い

Next.jsのfetch関数は、データフェッチのキャッシュ挙動を細かく制御できます。

  • デフォルト (force-cache): fetchは自動的にリクエストの結果をキャッシュし、次回以降のリクエストではキャッシュされたデータを使用します。ビルド時または最初のアクセス時にフェッチされたデータがキャッシュされ、後続のリクエストで再利用されます。
  • { cache: 'no-store' }: このオプションを指定すると、fetchはキャッシュを完全に無視し、常にオリジンから最新のデータをフェッチします。頻繁に更新されるデータや、パーソナライズされたデータに適しています。
    ts
    // app/dashboard/page.tsx (Server Component)
    async function getDynamicData() {
      const res = await fetch('https://api.example.com/dynamic-data', { cache: 'no-store' });
      return res.json();
    }
  • { next: { revalidate: <seconds> } }: 指定した秒数だけキャッシュを保持し、その期間が過ぎるとバックグラウンドでデータを再フェッチします(ISRに近い挙動)。これにより、常に新鮮なデータを保ちつつ、高いパフォーマンスを維持できます。
    ts
    // app/products/[id]/page.tsx (Server Component)
    async function getProduct(id: string) {
      const res = await fetch(`https://api.example.com/products/${id}`, { next: { revalidate: 3600 } }); // 1時間キャッシュ
      return res.json();
    }

export const revalidate

これはページまたはレイアウトレベルで、そのセグメントとその子孫に対するキャッシュの再検証間隔を定義します。

  • export const revalidate = <seconds>: 指定した秒数ごとにページまたはレイアウトのデータを再検証します。0を指定すると常に最新のデータをフェッチします。静的レンダリング(Static Rendering)されたページでISRのような挙動を実現します。
  • export const revalidate = 0: 常に動的なレンダリングを強制し、キャッシュを使用しません。
ts
// app/blog/[slug]/page.tsx
export const revalidate = 60; // 60秒ごとにこのページのデータを再検証

async function getBlogPost(slug: string) {
  const res = await fetch(`https://api.example.com/blog/${slug}`);
  return res.json();
}

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const post = await getBlogPost(params.slug);
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  );
}

dynamic = 'force-dynamic'

このオプションは、特定のルートセグメントを完全に動的にレンダリングすることを強制します。これにより、そのセグメント内のすべてのデータフェッチはキャッシュされず、常に最新のデータが取得されます。

  • export const dynamic = 'force-dynamic': ページまたはレイアウト全体が動的レンダリングに切り替わります。これは主に、リクエストごとに内容が大きく変わるページや、認証情報に基づいてパーソナライズされるページで使用されます。
ts
// app/user/[id]/profile/page.tsx
export const dynamic = 'force-dynamic'; // このページは常に動的にレンダリングされる

async function getUserProfile(id: string) {
  // 認証情報に基づいてユーザーデータをフェッチ
  const res = await fetch(`https://api.example.com/users/${id}/profile`, { cache: 'no-store' });
  return res.json();
}

export default async function UserProfilePage({ params }: { params: { id: string } }) {
  const profile = await getUserProfile(params.id);
  return (
    <div>
      <h1>{profile.name}のプロフィール</h1>
      <p>{profile.bio}</p>
    </div>
  );
}

これらのキャッシュ制御を適切に設計しないと、古い情報が表示されたり、不必要なデータフェッチによってパフォーマンスが低下したりする原因となります。開発段階から本番でのキャッシュ戦略を意識することが重要です。

5. 画像最適化・フォント・middlewareで起きやすい問題

画像最適化 (next/image)

Next.jsのnext/imageコンポーネントは、画像の最適化と配信を自動的に処理し、パフォーマンスを向上させます。VercelではImage CDNが利用され、高速な画像配信が可能です。

  • domainsremotePatterns の設定: 外部ドメインの画像をnext/imageで使用する場合、next.config.jsにそのドメインをimages.domainsまたはimages.remotePatternsとして許可する必要があります。これを怠ると、本番環境で画像が表示されなくなります。remotePatternsはより柔軟な正規表現での指定が可能です。
    javascript
    // next.config.mjs
    /** @type {import('next').NextConfig} */
    const nextConfig = {
      images: {
        remotePatterns: [
          {
            protocol: 'https',
            hostname: '**.example.com', // example.comとそのサブドメインを許可
          },
          {
            protocol: 'https',
            hostname: 'cdn.another-service.com',
          },
        ],
      },
    };
    
    export default nextConfig;
  • 画像ファイルの最適化: ローカルの静的画像はビルド時に自動的に最適化されますが、ユーザーがアップロードするような動的な画像は、Vercel Image CDNが最適化できるように、サポートされているフォーマットと適切なサイズで提供される必要があります。

フォント (next/font)

next/fontは、自動的にフォントを最適化し、外部ネットワークリクエストを排除してレイアウトシフトを防ぎます。

  • フォントファイルサイズの管理: カスタムフォントを使用する場合、フォントファイルが肥大化するとページのロード時間に影響を与えます。必要な文字セットのみをサブセット化するなどの工夫が必要です。
  • Flash Of Unstyled Text (FOUT)/Flash Of Invisible Text (FOIT) の回避: next/fontはこれらの問題を自動で軽減しますが、displayオプションなどを適切に設定することで、より制御されたフォント表示が可能です。

Middleware

Middlewareは、リクエストが完了する前に実行されるコードです。認証、リダイレクト、A/Bテストなど、様々な用途に利用できます。

  • Edge Runtimeでの実行: MiddlewareはEdge Runtimeで実行されるため、Node.js APIの一部は利用できません。重い処理や複雑なロジックは避け、軽量な処理に限定する必要があります。
  • パフォーマンスへの影響: すべてのリクエストに対して実行されるため、Middlewareの処理が遅いとアプリケーション全体のパフォーマンスに悪影響を与えます。可能な限り高速な処理を心がけ、外部サービスへの呼び出しは最小限に留めるべきです。
  • 環境変数の扱い: Middlewareで環境変数を使用する場合、Vercelダッシュボードで設定されたものが自動的に注入されます。NEXT_PUBLIC_が付かない環境変数はMiddlewareからは参照できません。
  • Cookieとヘッダーの操作: NextRequestオブジェクトを通じてCookieやヘッダーを読み書きできますが、変更はNextResponse.next()またはNextResponse.redirect()のオプションとして行う必要があります。
ts
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth_token');

  // 認証トークがない場合、ログインページにリダイレクト
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    const url = request.nextUrl.clone();
    url.pathname = '/login';
    return NextResponse.redirect(url);
  }

  // 特定のパスへのアクセスを許可する例
  if (request.nextUrl.pathname.startsWith('/admin')) {
    // 管理者権限チェックなど
    // ...
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], // 特定のパスを除外
};

6. 実践例1: App Router + Route Handlersの構成例

App RouterとRoute Handlersは、Web APIの構築とサーバーサイドのデータフェッチを簡素化します。ここでは、異なるキャッシュ戦略を持つRoute Handlersの例を示します。

例1:頻繁に更新されるニュースフィードAPI

このRoute Handlerは、60秒ごとにデータを再検証します。これにより、ユーザーにはほぼ最新のニュースが提供されつつ、APIへのリクエスト負荷を軽減します。

ts
// app/api/news/route.ts
import { NextResponse } from 'next/server';

export const revalidate = 60; // 60秒ごとにキャッシュを再検証

export async function GET() {
  const res = await fetch('https://external-news-api.com/latest', {
    headers: {
      'Authorization': `Bearer ${process.env.NEWS_API_KEY}`,
    },
    // ここでcache: 'no-store'を指定しない限り、revalidateが優先される
  });
  const data = await res.json();

  return NextResponse.json(data);
}

例2:リアルタイム性が求められる株価情報API

このRoute Handlerは、常に最新のデータをフェッチし、キャッシュを全く利用しません。dynamic = 'force-dynamic'は、このAPIが常に動的にレンダリングされることを保証します。

ts
// app/api/stocks/[symbol]/route.ts
import { NextResponse } from 'next/server';

export const dynamic = 'force-dynamic'; // このRoute Handlerは常に動的レンダリング

export async function GET(request: Request, { params }: { params: { symbol: string } }) {
  const { symbol } = params;
  const res = await fetch(`https://realtime-stock-api.com/quote/${symbol}`, {
    cache: 'no-store', // キャッシュを無効化
    headers: {
      'X-API-KEY': process.env.STOCK_API_KEY as string,
    },
  });

  if (!res.ok) {
    return NextResponse.json({ error: 'Failed to fetch stock data' }, { status: res.status });
  }

  const data = await res.json();
  return NextResponse.json(data);
}

これらの例から、Route HandlersとVercelの組み合わせでは、APIの要件に応じて柔軟なキャッシュ戦略とランタイム選択が可能であることがわかります。

7. 実践例2: 外部API/DB接続を含む本番チェックリスト

外部サービスやデータベースとの接続は、本番環境での安定性とパフォーマンスに直結します。デプロイ前に以下の点をチェックしましょう。

  • 環境変数の完全性:
    • 全ての外部APIキー、データベース接続文字列、シークレットがVercelの環境変数に設定されていることを確認してください。
    • 特に、ビルド時とランタイム時の環境変数の違いを理解し、正しいタイミングで値が利用されるように設定します。
  • APIエンドポイントのセキュリティ:
    • 外部APIへのリクエストは、機密情報がクライアントサイドに露出しないように、必ずサーバーサイド(Route HandlersやServer Components)で行います。
    • CORS設定が適切か、認証ヘッダーが正しく付与されているかを確認します。
  • データベース接続の最適化:
    • サーバーレス環境では、データベースへの接続プールを適切に管理することが重要です。PostgreSQLやMySQLなどのリレーショナルデータベースでは、接続を確立するオーバーヘッドが大きいため、接続プールミドルウェア(例: Prismaのpool設定、connection-string-uri-parserなど)を利用してコールドスタート時のパフォーマンス劣化を防ぎます。
    • VercelのPostgreSQL/KV StoreといったManaged Databaseサービスを利用することで、このあたりの管理が簡素化されます。
  • リージョン選択:
    • 外部APIやデータベースのホストリージョンとVercelのデプロイリージョンを可能な限り近くすることで、ネットワークレイテンシを最小限に抑えられます。Vercelでは、プロジェクト設定でデフォルトのServerless Function Regionを指定できます。
  • エラーハンドリングとロギング:
    • 外部サービスからのエラー応答や、データベース接続エラーを適切にハンドリングし、ユーザーに分かりやすいメッセージを返すようにします。
    • Vercelのダッシュボードや外部のオブザーバビリティツール(Sentry, Datadogなど)と連携し、サーバーレス関数のログが適切に記録・監視されるように設定します。
  • 接続タイムアウト:
    • 外部サービスへのリクエストには、適切なタイムアウトを設定します。これにより、応答のないAPIコールがアプリケーション全体のハングアップを引き起こすのを防ぎます。

8. パフォーマンス最適化の観点

本番運用では、Core Web Vitalsを含むパフォーマンス指標が重要です。

  • LCP (Largest Contentful Paint) / INP (Interaction to Next Paint):
    • LCP: ページのメインコンテンツが表示されるまでの時間。next/imageによる画像最適化、クリティカルCSSのインライン化、ウェブフォントのdisplay: optional設定などが有効です。
    • INP: ユーザー操作に対する応答性。不要なJavaScriptの削減、React.lazySuspenseによるコンポーネントの遅延ロード、重い処理のWeb Workerへのオフロードなどが効果的です。
  • Cold Start (コールドスタート):
    • サーバーレス関数がアイドル状態から初めてリクエストを処理するまでの起動時間。Node.js Runtimeの関数で特に顕著です。
    • 緩和策:
      • 関数バンドルサイズの削減(不必要な依存関係の削除)。
      • 重要なAPIエンドポイントに対しては、定期的に「ウォームアップ」リクエストを送信する。
      • 可能な限りEdge Runtimeを利用する。
  • Bundle肥大化:
    • クライアントサイドに送られるJavaScriptバンドルサイズが大きすぎると、ロード時間が長くなります。
    • next build --analyzeを実行してバンドルを分析し、大きなチャンクや重複する依存関係を特定します。
    • ダイナミックインポート (import()) を積極的に利用して、コンポーネネントやライブラリを必要な時にだけロードするようにします。
    • 不要なライブラリやコードを削除します。

9. 運用時の監視・ロールバック・Preview活用

監視 (Observability)

Vercelは、組み込みのAnalytics、Function Logs、Realtime Logsを提供し、アプリケーションの動作状況を把握できます。

  • Vercel Analytics: 訪問者数、Core Web Vitals、Functionsの実行時間などをグラフで確認できます。
  • Function Logs & Realtime Logs: サーバーレス関数やエッジ関数のログをリアルタイムで確認し、エラーの特定やデバッグに役立ちます。
  • 外部サービスとの連携: Sentry (エラー監視)、Datadog/New Relic (パフォーマンス監視) などの外部オブザーバビリティツールと連携することで、より高度な監視体制を構築できます。

ロールバック

Vercelは、以前のデプロイメントに瞬時にロールバックできる機能を提供しています。

  • 安全なデプロイ: 新しいデプロイメントで問題が発生した場合でも、以前の安定したバージョンにすぐに戻せるため、安心してリリースを行えます。
  • Vercelダッシュボード: デプロイメント履歴から、いつでも任意のデプロイメントを本番環境に再プロモートできます。

Vercel Preview Deploymentの活用

Preview Deploymentは、開発ブランチやプルリクエストごとに自動的にデプロイされる機能です。本番環境に影響を与えずに、変更内容を検証するのに非常に役立ちます。

  • レビュープロセス: チームメンバーやステークホルダーが、変更が本番にデプロイされる前に実際の動作を確認できます。
  • 自動テスト: Preview Deployment上でE2Eテストやパフォーマンステストを実行することで、変更による潜在的な問題を早期に発見できます。
  • フィードバックの収集: Preview URLを共有することで、ユーザーからのフィードバックを素早く収集し、開発サイクルを加速できます。
bash
# プルリクエスト作成時に自動でPreview Deploymentが作成される(Vercel連携時)
# 手動でPreview Deploymentを作成する場合 (Vercel CLI使用)
vercel deploy --prebuilt --prod=false

10. まとめ

Next.jsとVercelは強力な組み合わせですが、その真価を発揮するには、本番デプロイにおける特有の課題を理解し、適切な戦略を講じることが不可欠です。環境変数の管理、Node.jsとEdge Runtimeの使い分け、詳細なキャッシュ戦略、そして画像最適化やMiddlewareのベストプラクティスは、アプリケーションのパフォーマンスと安定性を大きく左右します。

また、Vercelが提供する監視ツールや、瞬時のロールバック、Preview Deploymentといった機能は、開発から運用までのライフサイクル全体をサポートし、安全で効率的な本番運用を実現します。これらの知識を活かし、読者の皆様がVercel上でのNext.jsアプリケーションの本番運用を成功させる一助となれば幸いです。

11. 参考リンク

AI が自動生成した技術記事をまとめたテックブログ