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

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

金曜の夜 9 時、プロダクトマネージャーがグループチャットにスクショを送ってきました。ユーザーテストの動画では、テスターがスマホでブログ一覧を開くと、Loading アイコンが 5 秒も回り続け、画面は真っ白。右下には「いつの時代のサイト?」というコメント。

Chrome DevTools を開くと、API リクエストだけで 3200ms。少し焦りました。この API が遅いのは知っていましたが、手を付けられず——ここまで酷いとは。

その後 2 日間 Next.js のパフォーマンス最適化を調べ、思ったよりシンプルでした。キャッシュ戦略を正しく選び、ストリーミングとエッジコンピューティングを組み合わせれば、応答時間は 3 秒から 500ms 以内に。何より大事だったのは、どの場面でどれを使うかをはっきりさせること——技術の一覧より、こちらの方がはるかに重要です。

本記事では 3 つの手法を解説します。キャッシュ戦略の選び方、ストリーミングの実装、Edge Functions が向くシーン。コードは実際に動かしたもの、性能データも実測値です。そのまま使えます。

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

まずよくあるボトルネックから。3 秒かかっていた API を調べると、典型的な問題がいくつかありました。

DB クエリが最適化されていない。記事ごとに著者情報を個別クエリするループ——定番の N+1 問題。100 記事なら 100 回 DB アクセス。遅くて当然です。インデックスすらないテーブルもありました。

キャッシュがまったくない。ユーザーがリロードするたび、サーバーは DB 検索・計算・フォーマットを最初からやり直し。設定情報は月 1 回程度しか変わらないのに、毎秒再計算していました。

全データを一括返送。100 記事の完全な内容(本文含む)を一度に返し、JSON は 2MB 超。転送だけで 1 秒。一覧に必要なのはタイトルと要約だけなのに。

サーバーの地理的位置。サーバーは米国西海岸。国内ユーザーは往復だけで 200ms 以上。GFW の影響も——ここでは割愛します。

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

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

以前は Next.js が自動で多くをキャッシュしてくれました。便利に見えて、実際は「キャッシュされてる? いつまで? どう消す?」と混乱しがち。データは更新済みなのに古い表示——デバッグして初めてキャッシュが原因、ということも。

今は、何をいつまでキャッシュするか明示する必要があります。手間は増えますが、何が起きているか把握でき、制御しやすくなりました。

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

問題が見えれば、改善の軸も明確です。

  1. キャッシュ:すでにやったことを繰り返さない
  2. ストリーミング:全部の準備を待たず、できた分から送る
  3. エッジコンピューティング:サーバーをユーザーに近づける

順に見ていきます。

キャッシュ戦略:方法を選べば効果は大きい

Next.js のキャッシュは 4 種類——Request Memoization、Data Cache、Full Route Cache、Router Cache。初見は多いですが、全部覚える必要はありません。

API Routes で最も使うのは Data Cache——DB クエリ結果や外部 API レスポンスをキャッシュする仕組みです。

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

サイト設定やカテゴリ一覧など、ほとんど変わらないデータは、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% 短縮
カテゴリ一覧にキャッシュを入れた結果、大半のリクエストはキャッシュ返却で DB に到達しない

シナリオ 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'
    }
  })
}

賢い戦略です。キャッシュ(多少古くても)を先に返し、裏で非同期更新。ユーザーは待たず、データも極端に古くなりません。

s-maxage=60 は 60 秒間 fresh、stale-while-revalidate=300 は期限切れ後 300 秒間は古いデータを返しつつバックグラウンド更新、という意味です。

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

株価やチャットなど、リアルタイム性が重要なデータはキャッシュしない。完全に no-store にするか、WebSocket や Server-Sent Events でプッシュします。

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())
}

更新後、関連キャッシュが即無効化され、次回リクエストで最新データが取れます。

よくある落とし穴

罠 1:過度なキャッシュ。注文ステータスを 1 時間キャッシュし、決済後も半日ステータスが更新されない——キャッシュ時間はデータ特性に合わせ、長いほど良いわけではありません。

罠 2:キャッシュウォームアップの忘れ。初回リクエストはキャッシュ空で遅いまま。デプロイ後にホットデータ用 API を一度叩き、キャッシュを温めておくと効果的です。

罠 3:キャッシュキー設計ミス。ユーザー A のデータがキャッシュされ、ユーザー B にも返る——キーにユーザー ID など識別子を必ず含めましょう。

ストリーミングレスポンス:大容量転送のもたつきを解消

キャッシュは再計算を減らしますが、計算自体が重い・データ量が大きい場合はストリーミングの出番です。

ストリーミングとは?

従来の API はレストランのコース料理——全部できてから一斉に出る。10 品あれば、最遅の 1 品を待つ。

ストリーミングは、できた料理から順に出す。総時間は近くても、客は早く食べ始められる。空腹のまま待たない。

ユーザー体感では、「白画面 3 秒」から「500ms で先頭数件が見え、先に読める」へ。体験はまったく違います。

いつストリーミングを使う?

典型例は次のとおりです。

  1. 長いリスト:商品一覧、記事一覧、検索結果
  2. AI 生成コンテンツ:ChatGPT のタイプライター表示もストリーミング
  3. 大ファイル処理:Excel エクスポート、PDF 生成
  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'
    }
  })
}

複雑ではありません。要点は次の 4 点です。

  1. ReadableStream を作る
  2. start 内でデータを分割取得
  3. controller.enqueue() で各バッチを送信
  4. 完了後 controller.close()

フロントエンドでの受信

バックエンドがストリームなら、フロントも対応が必要です。

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) {
      console.log('数据接收完毕')
      break
    }

    // 解码数据
    const chunk = decoder.decode(value)

    // 解析 JSON(每行一个)
    const posts = JSON.parse(chunk)
    allPosts = [...allPosts, ...posts]

    // 实时更新 UI
    updatePostList(allPosts)
  }
}

ページを開くと、リストが段階的に埋まり、長時間の白画面を避けられます。

実測での効果比較

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

2800ms → 500ms
初回表示時間
最適化前は 2800ms 待って一括表示、最適化後は 500ms で先頭 20 件表示。すぐ閲覧開始

総時間は 1300ms ほど短縮しただけですが、体感は倍以上速く感じられます。500ms で操作でき、残りはコンテンツを読んでいる時間——待ち時間ではありません。

小技:仮想スクロール

データがさらに多い場合は、仮想スクロール(Virtual Scrolling)と組み合わせましょう。表示領域だけ描画し、1000 件受信してもカクつきにくくなります。

React なら react-windowreact-virtualized、Vue なら vue-virtual-scroller が使えます。

Edge Functions:API をユーザーの目の前へ

キャッシュとストリーミングはソフトウェア側の最適化。もっと直接的な方法もあります——サーバーをユーザーに近づけること。

物理距離の影響

ネットワーク遅延の多くは物理距離由来です。光速に限界があり、北京から米国西海岸まで往復は最低 200ms 程度——物理法則で、コード最適化では消せません。

以前はサーバーを固定リージョン(例:AWS 東京)に置くしかなく、近いユーザーは速く、遠いユーザーは遅い、という構図でした。

Edge Functions は、コードを世界中の数十〜数百ノードに配置し、ユーザーに最も近いノードへルーティング。北京ユーザーは北京ノード、ニューヨークユーザーはニューヨークノード——遅延は 50ms 以内に抑えやすくなります。

Edge Runtime と Node.js Runtime の違い

Next.js の API Routes はデフォルトで Node.js Runtime。fscrypto、DB 接続など Node.js API が使えます。

Edge Runtime は V8 ベース(Chrome と同系)で、完全な Node.js 環境ではありません。起動は超高速(0〜5ms)ですが、使える API は限られます。

簡単な比較:

特性Node.js RuntimeEdge Runtime
起動速度100-500ms0-5ms
利用可能 APINode.js フル制限あり(Web 標準 API のみ)
向く用途複雑な業務ロジック、DB 操作軽量ロジック、認証、プロキシ
グローバル遅延デプロイ地点依存世界中 <50ms
メモリ上限比較的高い低い(128MB)

Edge Functions が向くシーン

すべての API を Edge に移す必要はありません。典型例は次のとおりです。

シナリオ 1:認証・認可

Edge に最適なのは認証です。JWT 検証や API キー確認など軽量チェックをエッジで完結させ、無効リクエストを中心サーバーまで届けません。

// app/api/auth/route.js
export const runtime = 'edge'

export async function GET(request) {
  const token = request.headers.get('authorization')

  if (!token) {
    return new Response('Unauthorized', { status: 401 })
  }

  // 验证 token(可以用 jose 库,支持 Edge)
  const isValid = await verifyToken(token)

  if (!isValid) {
    return new Response('Invalid token', { status: 401 })
  }

  return Response.json({ user: 'authenticated' })
}
200ms → 20ms
認証レイテンシ 90% 削減
エッジで token を高速検証。無効リクエストは中心サーバーに到達せず負荷軽減

シナリオ 2:地域別パーソナライズ

ユーザー IP から国を判定し、言語・通貨・おすすめコンテンツを出し分けます。

export const runtime = 'edge'

export async function GET(request) {
  // 获取用户地理位置(Vercel 会自动注入)
  const country = request.geo?.country || 'US'
  const city = request.geo?.city || 'Unknown'

  // 根据位置返回不同内容
  const content = getLocalizedContent(country)

  return Response.json({
    country,
    city,
    content,
    currency: country === 'CN' ? 'CNY' : 'USD'
  })
}

DB 不要。エッジで処理でき、非常に速いです。

シナリオ 3:API プロキシ

フロントが複数外部 API を叩く場合、Edge で集約し、クライアントのリクエスト回数を減らせます。

export const runtime = 'edge'

export async function GET(request) {
  // 并行请求多个 API
  const [weather, news] = await Promise.all([
    fetch('https://api.weather.com/...'),
    fetch('https://api.news.com/...')
  ])

  return Response.json({
    weather: await weather.json(),
    news: await news.json()
  })
}

ユーザーは 1 リクエスト。バックエンドは並列処理で総遅延を大きく下げられます。

シナリオ 4:A/B テスト

エッジで返すバージョンを決め、メインアプリを触らずに実験できます。

export const runtime = 'edge'

export async function GET(request) {
  const userId = request.headers.get('x-user-id')

  // 简单的 A/B 分流逻辑
  const variant = parseInt(userId) % 2 === 0 ? 'A' : 'B'

  const content = variant === 'A' ? getContentA() : getContentB()

  return Response.json({ variant, content })
}

Edge Functions の制限

Edge が万能ではない理由——制限が多いからです。

制限 1:Node.js 専用 API が使えない

fspathchild_process などは不可。コードにこれらがあると Edge 移行でエラーになります。

制限 2:DB 接続

従来の pgmysql2 は Node.js の net に依存するため使えません。HTTP ベースの接続が必要です。例:

  • Prisma Data Proxy
  • PlanetScale(MySQL)
  • Supabase(PostgreSQL)
  • Redis(HTTP API 対応)

制限 3:メモリと実行時間

Edge Functions は通常、メモリ 128MB・実行 30 秒上限。重い計算や大量データ処理には向きません。

おすすめ:ハイブリッド構成

どちらか一方に寄せる必要はありません。私のやり方は次のとおりです。

  • エッジ層(Edge):認証、地域判定、シンプルなプロキシ
  • 中心層(Node.js):複雑な業務ロジック、DB 操作、ファイル処理

Edge で無効・単純リクエストを弾き、複雑なものだけ中心へ。遅延を下げつつ Edge の制限も回避できます。

性能実測

Medium のベンチマーク研究によると:

  • Vercel Edge Functions:平均レイテンシ 48.3ms
  • Cloudflare Workers(カスタム):平均 36.37ms
  • 従来 Node.js API(単一リージョン):平均 200〜500ms

Edge は確かに速いですが、効果はユーザー分布次第。ユーザーが国内集中なら、国内の従来サーバーの方が速い場合もあります。

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

3 つの技術を組み合わせ、冒頭の遅いブログ一覧 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)
}

性能データ:

  • 応答時間:2800ms
  • JSON サイズ:2.3MB
  • ユーザー体験:3 秒白画面

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

N+1 を解消し、必要フィールドだけ返します。

export async function GET() {
  const posts = await db.post.findMany({
    take: 100,
    select: {
      id: true,
      title: true,
      summary: true,  // 只要摘要,不要全文
      createdAt: true,
      author: {
        select: { name: true, avatar: true }
      }
    }
  })

  return Response.json(posts)
}

効果:応答 800ms、JSON は 2.3MB → 180KB。

ステップ 2:キャッシュ追加

記事一覧は頻繁には変わらないので、5 分キャッシュ。

export async function GET() {
  const posts = await db.post.findMany({
    // ... 同上
  }, {
    next: {
      revalidate: 300,  // 缓存 5 分钟
      tags: ['posts']
    }
  })

  return Response.json(posts)
}

公開時にキャッシュをクリア:

// app/api/posts/publish/route.js
import { revalidateTag } from 'next/cache'

export async function POST(request) {
  const newPost = await request.json()
  await db.post.create({ data: newPost })

  // 清除文章列表缓存
  revalidateTag('posts')

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

効果:キャッシュヒット時 50ms、サーバー負荷 90% 削減。

ステップ 3:ストリーミングへ変更

かなり速くなりましたが、初回(キャッシュミス)は依然 800ms。ストリーミングに切り替え:

export async function GET() {
  const encoder = new TextEncoder()

  const stream = new ReadableStream({
    async start(controller) {
      const batchSize = 20

      for (let page = 0; page < 5; page++) {
        const posts = await db.post.findMany({
          skip: page * batchSize,
          take: batchSize,
          select: { /* 同上 */ }
        })

        const chunk = JSON.stringify(posts) + '\n'
        controller.enqueue(encoder.encode(chunk))
      }

      controller.close()
    }
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'application/x-ndjson', // Newline Delimited JSON
      'Cache-Control': 's-maxage=300, stale-while-revalidate=600'
    }
  })
}

効果:最初のバッチが 300ms で返り、すぐ閲覧開始。総時間 800ms でも待ち感は小さい。

ステップ 4:Edge 層認証(任意)

認証が必要なら、Edge で一次検証:

// app/api/posts/route.js (Edge 鉴权层)
export const runtime = 'edge'

export async function GET(request) {
  const token = request.headers.get('authorization')

  if (!token) {
    return new Response('Unauthorized', { status: 401 })
  }

  // 验证通过,转发到实际 API(Node.js Runtime)
  return fetch(`${process.env.API_BASE_URL}/posts/internal`, {
    headers: { authorization: token }
  })
}

無効リクエストはエッジで遮断。中心サーバーまで届きません。

最適化効果の比較

指標最適化前最適化後改善
初回アクセス応答時間2800ms300ms(初回バッチ)89% ↓
キャッシュヒット応答時間-50ms98% ↓
JSON サイズ2.3MB180KB92% ↓
ユーザー操作可能時間2800ms300ms89% ↓
サーバー負荷100%10%90% ↓

「いつの時代のサイト?」という声は、なくなりました。

性能監視と継続的最適化

最適化で終わりではありません。継続監視しないと効果も問題も見えません。

重要指標

私が見ている指標は次の 4 つです。

  1. 応答時間分布(P50、P95、P99)

    • P50(中央値):半数のユーザー体験
    • P95:95% のユーザー体験
    • P99:最遅 1%(異常の兆候になりやすい)
  2. キャッシュヒット率

    • 70% 未満なら戦略見直し
    • 95% 超ならキャッシュが長すぎて鮮度不足の可能性
  3. エラー率

    • 最適化後に上がっていないか確認
    • ストリーミングは途中失敗に注意
  4. 地域分布

    • 地域別レイテンシ差
    • Edge Functions が必要か判断材料

監視ツール

Vercel Analytics:Vercel デプロイなら標準の性能監視。各 API の応答時間分布が見られます。

Next.js Instrumentation API(2026 年新機能):コードに計測ポイントを挿入できます。

// instrumentation.js
export function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    require('./monitoring')
  }
}

// monitoring.js
export function onRequestEnd(info) {
  console.log(`API ${info.url} took ${info.duration}ms`)

  // 发送到监控平台
  sendToMonitoring({
    url: info.url,
    duration: info.duration,
    status: info.status
  })
}

カスタムログ:単純ですが有効です。

export async function GET() {
  const start = Date.now()

  const data = await fetchData()

  const duration = Date.now() - start
  console.log(`API /posts took ${duration}ms`)

  return Response.json(data)
}

継続最適化の提案

  1. キャッシュ戦略の定期見直し:業務が変われば戦略も変える
  2. A/B テスト:どちらが良いか不明なら試す
  3. 実データで調整:感覚ではなく監視データで判断

パフォーマンス最適化は継続プロセス。一度きりではありません。

まとめ

要点を整理します。

キャッシュ戦略:データ特性で選ぶ。静的は長キャッシュ、ユーザー向けは stale-while-revalidate、リアルタイムはキャッシュしない。更新後の無効化も忘れずに。

ストリーミング:データが大きい・計算が重いときの切り札。白画面で待たせず、早く内容を見せる。フロントは仮想スクロールと相性が良い。

Edge Functions:認証、地域判定、API プロキシなど軽量ロジック向き。複雑業務は Node.js Runtime と併用が正解。

最適化は一気に完璧を目指さなくて大丈夫。最も遅い API から 3 手法を試し、実測して調整——段階的に進めましょう。

私のブログ一覧 API は 3 秒から 300ms へ。ユーザー体験の改善は目に見えて大きかったです。遅い API を 1 つ選び、今日から試してみてください。質問はコメントで——一緒に進めましょう。

FAQ

Next.js API のキャッシュはいつ無効になりますか?
無効化には 3 つのパターンがあります。1) 時間経過(revalidate で設定した時間が来た)、2) 手動無効化(revalidateTag や revalidatePath を呼ぶ)、3) ユーザーによる強制更新(Ctrl+Shift+R)。よく使うのは 1 と 2 です。データ更新頻度に合わせて revalidate 時間を設定しましょう。
ストリーミングレスポンスはすべての API に向いていますか?
いいえ。データ量が多い(長いリストなど)か、計算に時間がかかる(AI 生成など)場面向きです。データが小さく計算も速いなら、従来型レスポンスで十分。目安は、応答時間が 1 秒超、または JSON が 500KB 超のときに検討してください。
Edge Functions にはどんな制限がありますか?
主に 3 つ。1) Node.js 専用 API(fs、child_process など)が使えない、2) DB 接続は HTTP ベース(Prisma Data Proxy など)が必要、3) メモリ 128MB・実行時間 30 秒の上限。認証やプロキシなど軽量ロジック向きで、複雑な業務は Node.js Runtime が適します。
キャッシュ戦略はどう選べばいいですか?
データ更新頻度で決めます。静的データ(設定、カテゴリ)は長キャッシュ(1 時間以上)、ユーザーデータ(プロフィール)は stale-while-revalidate(60 秒 fresh + 300 秒バックグラウンド更新)、リアルタイムデータ(株価)はキャッシュしないか WebSocket。キャッシュが長いほど性能は上がりますが、古いデータのリスクも増えます。
最適化後の効果はどう検証しますか?
4 指標を見ます。1) 応答時間(P50、P95、P99)、2) キャッシュヒット率(目標 70〜95%)、3) エラー率(最適化で上がっていないか)、4) 地域別レイテンシ。Vercel Analytics、Next.js Instrumentation API、カスタムログが使えます。最適化前後の A/B テストも忘れずに。

6分で読めます · 公開日: 2026年1月5日 · 更新日: 2026年6月8日

関連記事

コメント

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