言語を切り替える
テーマを切り替える

Next.js API パフォーマンス最適化完全ガイド:キャッシュ戦略、ストリーミング、エッジコンピューティング

金曜日の夜9時、プロダクトマネージャーからチャットでスクリーンショットが送られてきました。ユーザビリティテストの動画で、ブログの一覧ページを開いたとき、ローディングアイコンがなんと5秒間も回り続け、画面は真っ白なまま。右下にはテスターの辛辣なコメントが。「いつの時代のサイトですか?」

Chrome DevTools を開いて確認すると、API リクエストだけで 3200ms もかかっていました。正直焦りました。遅いのは知っていましたが、ここまで酷いとは。最適化を後回しにしていたツケが回ってきたのです。

その後2日間かけて Next.js のパフォーマンス最適化に取り組み、結果としてレスポンスタイムを 3秒から 500ms 以内に短縮することに成功しました。重要なのは技術そのものよりも、いつどの技術を使うべきかを理解することでした。

今日は、私が実践した3つの秘策——キャッシュ戦略の選び方、ストリーミングレスポンスの実装、Edge Functions の活用シーン——についてお話しします。すべてのコードは実戦で検証済みです。

なぜあなたの Next.js API はこんなに遅いのか?

まずは一般的なボトルのネックから。私が排查した3秒の遅延の原因は、いくつかの典型的な問題の積み重ねでした。

最適化されていないデータベースクエリ。コードを見ると、記事ごとに著者情報を個別にクエリするループがありました。いわゆる N+1 問題です。100記事あれば100回のDBリクエスト。遅くて当たり前です。さらにインデックスすら貼られていないテーブルもありました。

キャッシュの欠如。ユーザーがリロードするたびに、サーバーはデータベースを検索し、計算し、フォーマットし直していました。設定情報など月に1回しか変わらないデータさえ、毎秒再計算されていたのです。

全データの一括返送。API は100記事分の完全なコンテンツ(本文含む)を一度に返していました。JSON サイズは 2MB 超。ネットワーク転送だけで1秒かかります。一覧ページに必要なのはタイトルと要約だけなのに。

サーバーの物理的位置。サーバーは米国西海岸にあり、日本からのアクセスは往復だけで 200ms 以上かかります。

Next.js 16 がもたらしたキャッシュの変化

2025年10月、Next.js 16 で重要な変更がありました。暗黙的キャッシュから明示的キャッシュへの移行です。

以前の Next.js は自動的に多くのものをキャッシュしてくれましたが、「これキャッシュされてるの?いつまで?どうやって消すの?」と混乱を招くことが多く、デバッグが困難でした。
現在は、何をいつまでキャッシュするかを明示的に指定する必要があります。手間は増えましたが、制御性は格段に向上しています。

パフォーマンス改善の3つの方向性

問題が分かれば対策は明確です。

  1. キャッシュ: 既にやったことを繰り返さない
  2. ストリーミング: 全部終わるのを待たずに、できたものから送る
  3. エッジ計算: サーバーをユーザーの近くへ持っていく

これらを順に解説します。

キャッシュ戦略:正しい方法を選べば効果絶大

Next.js のキャッシュ機構は複雑(Request Memoization, Data Cache, Full Route Cache, Router Cache…)に見えますが、API Routes において最も重要なのは Data Cache です。これは DB クエリ結果や外部 API のレスポンスをキャッシュする機能です。

シナリオ1:静的データのキャッシュ

サイト設定やカテゴリ一覧など、滅多に変わらないデータは長時間キャッシュしましょう。

// app/api/categories/route.js
export async function GET() {
  const data = await fetch('https://api.example.com/categories', {
    next: { revalidate: 3600 } // 1時間キャッシュ
  })

  return Response.json(await data.json())
}

これだけで十分です。revalidate: 3600 は「1時間はキャッシュを使い、それ以降はバックグラウンドで再取得する」という意味です。

500ms → 50ms
レスポンス時間 90% 短縮

シナリオ2:ユーザー関連データのキャッシュ

プロフィール情報などは頻繁には変わりませんが、ずっと古いままでは困ります。ここで stale-while-revalidate 戦略が輝きます。

// app/api/user/profile/route.js
export async function GET(request) {
  const user = await getUserFromDB()

  return new Response(JSON.stringify(user), {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 's-maxage=60, stale-while-revalidate=300'
    }
  })
}

この戦略は非常に賢いです。キャッシュがあれば(多少古くても)即座にそれを返し、裏で非同期に新しいデータを取得してキャッシュを更新します。ユーザーは待ち時間ゼロでデータを見られ、次回アクセス時には最新データになっています。

シナリオ3:リアルタイムデータはキャッシュしない

株価やチャットメッセージなど、リアルタイム性が命のデータはキャッシュしてはいけません。

export async function GET() {
  const price = await getStockPrice()

  return new Response(JSON.stringify(price), {
    headers: {
      'Cache-Control': 'no-store' // キャッシュしない
    }
  })
}

キャッシュの無効化:データ更新時の対応

ユーザーがプロフィールを更新したのに、キャッシュが古いままでは困ります。更新時にはキャッシュをパージ(無効化)する必要があります。

Next.js は revalidateTagrevalidatePath を提供しています:

// app/api/user/update/route.js
import { revalidateTag } from 'next/cache'

export async function POST(request) {
  const data = await request.json()
  await updateUserProfile(data)

  // ユーザープロファイルに関連するキャッシュを無効化
  revalidateTag('user-profile')

  return Response.json({ success: true })
}

これに対応して、データ取得時にタグを付けておきます:

export async function GET() {
  const data = await fetch('db-api/user', {
    next: {
      revalidate: 3600,
      tags: ['user-profile'] // タグ付け
    }
  })
  return Response.json(await data.json())
}

これで、更新APIが叩かれると即座に関連キャッシュが無効化され、次回は最新データが取得されます。

よくある落とし穴

罠1:過度なキャッシュ。注文ステータスを1時間キャッシュしてしまい、決済完了したのに「未払い」のまま…なんてことがないように。キャッシュ時間はデータの性質に合わせて慎重に。

罠2:キャッシュのキー設計ミス。ユーザーAのデータがキャッシュされ、ユーザーBにもそれが表示されてしまう(情報漏洩)。キャッシュキーには必ずユーザーIDなどを含めましょう。

ストリーミングレスポンス:大容量データの転送待ちをなくす

キャッシュは計算済みデータには有効ですが、計算そのものが重い場合やデータ量が莫大な場合はどうすればいいでしょう?ここで ストリーミング の出番です。

ストリーミングとは?

従来の API レスポンスはレストランのコース料理のようなもので、全ての料理が出来上がってから一度にテーブルに運ばれてくるイメージです。遅い料理があれば、客はひたすら待ちます。

ストリーミングは、出来た料理から順次運んでくるスタイルです。客(ユーザー)は最初の料理を食べ始められるので、待ち時間を短く感じます。

どんな時に使う?

  1. 長いリスト:商品一覧、記事一覧、検索結果
  2. AI生成:ChatGPT のように文字が徐々に出てくるあの動き
  3. 大容量ファイル:CSVエクスポートなど
  4. リアルタイムログ

Next.js での実装

ReadableStream を使用するのが一般的です。

// app/api/posts/stream/route.js
export async function GET() {
  const encoder = new TextEncoder()

  const stream = new ReadableStream({
    async start(controller) {
      // データを分割して取得(バッチ処理)
      for (let page = 0; page < 5; page++) {
        // 20件ずつ取得
        const posts = await fetchPostsFromDB({ page, limit: 20 })

        // チャンクとして送信
        const chunk = JSON.stringify(posts) + '\n'
        controller.enqueue(encoder.encode(chunk))

        // 処理時間のシミュレーション
        await new Promise(r => setTimeout(r, 100))
      }

      // 送信完了
      controller.close()
    }
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/plain; charset=utf-8',
      'Transfer-Encoding': 'chunked'
    }
  })
}

フロントエンドでの受信

バックエンドがストリームで送ってくるなら、フロントエンドもそれに対応して少しずつ読み込む必要があります。

async function fetchStreamData() {
  const response = await fetch('/api/posts/stream')
  const reader = response.body.getReader()
  const decoder = new TextDecoder()

  let allPosts = []

  while (true) {
    const { done, value } = await reader.read()

    if (done) break

    const chunk = decoder.decode(value)
    // JSONパースしてリストに追加し、UIを更新
    const posts = JSON.parse(chunk)
    allPosts = [...allPosts, ...posts]
    updatePostList(allPosts)
  }
}

実際の効果

ブログ一覧にストリーミングを導入した結果:

2800ms → 500ms
FCP(First Contentful Paint)

完了までの総時間は変わらなくても、体感速度は劇的に向上します。

Edge Functions:サーバーをユーザーの目の前へ

物理的な距離による遅延は、どんなにコードを最適化しても消せません。光の速度には限界があるからです。これを解決するのが Edge Functions です。

サーバーを世界中(エッジノード)に配置し、ユーザーに最も近いノードでコードを実行させます。

Edge Runtime とは?

Next.js のデフォルトは Node.js Runtime ですが、Edge Runtime は V8 エンジン(ブラウザと同じ)ベースの軽量環境です。

特性Node.js RuntimeEdge Runtime
起動速度100-500ms0-5ms
APIフル Node.js APIWeb 標準 API のみ
適用複雑なビジネスロジック軽量ロジック、認証、プロキシ
遅延デプロイ先に依存世界中どこでも <50ms

どんな時に Edge を使うべきか?

シナリオ1:認証・認可
JWT の検証など、DB アクセスを伴わない軽量なチェックはエッジに最適です。

// app/api/auth/route.js
export const runtime = 'edge' // エッジランタイム指定

export async function GET(request) {
  const token = request.headers.get('authorization')
  // トークン検証(jose ライブラリなどを使用)
  const isValid = await verifyToken(token)

  if (!isValid) {
    return new Response('Unauthorized', { status: 401 })
  }
  return Response.json({ user: 'authenticated' })
}
200ms → 20ms
認証レイテンシ 90% 削減

シナリオ2:地域判定とコンテンツ出し分け
ユーザーの IP アドレスから国を特定し、適切な言語や通貨のコンテンツを返します。

シナリオ3:API プロキシ
クライアントから複数の外部 API を叩く代わりに、エッジでまとめて叩いて結果をマージして返すBFF(Backend for Frontend)パターン。

Edge の制限

「全部 Edge にすればいいのでは?」と思うかもしれませんが、制限があります。

  1. Node.js 固有 API が使えないfschild_process は動きません。
  2. DB 接続:従来の TCP 接続型の DB ドライバは使えません。HTTP ベースの接続(Prisma Data Proxy, Supabase, PlanetScale など)が必要です。
  3. リソース制限:メモリや実行時間に厳しい制限があります。

結論:ハイブリッドが最強
エッジ層(認証、ルーティングなど)と、中央サーバー層(複雑な処理、DB操作)を組み合わせるのが賢い設計です。

総合実践:ブログ記事一覧 API の最適化

これまでの技術を組み合わせて、最初の「遅い API」をどう改善したか見てみましょう。

改善前

// app/api/posts/route.js
export async function GET() {
  // 問題1: 毎回フルスキャン、キャッシュなし
  const posts = await db.post.findMany({
    take: 100,
    include: {
      author: true, // 問題2: N+1 クエリになりがち
      tags: true
    }
  })
  // 問題3: 本文含む全データを返送
  return Response.json(posts)
}

ステップ1:クエリ最適化

必要なフィールドだけを取得します。

const posts = await db.post.findMany({
  select: {
    id: true,
    title: true,
    summary: true, // 本文は除外
    author: { select: { name: true, avatar: true } }
  }
})

これだけでデータサイズは 2MB → 180KB に。

ステップ2:キャッシュ導入

const posts = await db.post.findMany({
  // ...
}, {
  next: {
    revalidate: 300, // 5分キャッシュ
    tags: ['posts']
  }
})

ステップ3:ストリーミング化

初回アクセス(キャッシュミス時)の対策として、ストリーミングも実装(前述のコード参照)。

ステップ4:エッジでの事前認証(オプション)

エッジ層でトークンチェックを行い、有効なリクエストだけをこの API に通すように構成。

結果比較

指標改善前改善後向上率
初回応答時間2800ms300ms (最初のバッチ)89%
キャッシュ時応答-50ms98%
JSON サイズ2.3MB180KB92%
サーバー負荷100%10%90%

ユーザーからのクレームは称賛に変わりました。

まとめ

パフォーマンス最適化は一発勝負ではなく、継続的なプロセスです。

  1. キャッシュ:静的データは長く、動的データは賢くキャッシュする。
  2. ストリーミング:ユーザーを待たせないために、準備できたデータから見せる。
  3. エッジ計算:物理的な距離を縮めるために、ロジックを分散させる。

まずは一番遅い API をひとつ選び、これらの技術を適用してみてください。驚くほど改善するはずです。

FAQ

Next.js API キャッシュはいつ無効になりますか?
1) revalidate で設定した時間が経過した時、2) revalidateTag や revalidatePath を手動で呼び出した時、3) オンデマンド再検証(On-demand Revalidation)リクエストがあった時です。適切なキャッシュ期間の設定が重要です。
ストリーミングレスポンスはすべてのAPIに適していますか?
いいえ。計算に1秒以上かかる場合やデータ量が500KBを超えるような「重い」レスポンスに適しています。軽量なレスポンスなら通常のJSON返却の方がシンプルで高速です。
Edge Functions で普通のDBに繋げないのはなぜ?
Edge Functions はWeb標準APIに基づいており、Node.js の net モジュール(TCP接続に必要)を持たないためです。HTTP経由でアクセスできるデータベースやプロキシサービスを利用する必要があります。
効果はどうやって測定すればいいですか?
Vercel Analytics や Next.js の Instrumentation API を使って P50, P95, P99 のレスポンスタイムを監視しましょう。また、キャッシュヒット率も重要な指標です。

5 min read · 公開日: 2026年1月5日 · 更新日: 2026年1月22日

コメント

GitHubアカウントでログインしてコメントできます

関連記事