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 方向
問題が見えれば、改善の軸も明確です。
- キャッシュ:すでにやったことを繰り返さない
- ストリーミング:全部の準備を待たず、できた分から送る
- エッジコンピューティング:サーバーをユーザーに近づける
順に見ていきます。
キャッシュ戦略:方法を選べば効果は大きい
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 時間キャッシュし、その後自動更新。
シナリオ 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 は revalidateTag と revalidatePath を提供しています。
// 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 で先頭数件が見え、先に読める」へ。体験はまったく違います。
いつストリーミングを使う?
典型例は次のとおりです。
- 長いリスト:商品一覧、記事一覧、検索結果
- AI 生成コンテンツ:ChatGPT のタイプライター表示もストリーミング
- 大ファイル処理:Excel エクスポート、PDF 生成
- リアルタイムログ:ビルドログ、タスク進捗
データ量が多い、または計算に時間がかかるなら、検討する価値があります。
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 点です。
ReadableStreamを作るstart内でデータを分割取得controller.enqueue()で各バッチを送信- 完了後
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)
}
}
ページを開くと、リストが段階的に埋まり、長時間の白画面を避けられます。
実測での効果比較
ブログ一覧にストリーミングを入れた結果:
総時間は 1300ms ほど短縮しただけですが、体感は倍以上速く感じられます。500ms で操作でき、残りはコンテンツを読んでいる時間——待ち時間ではありません。
小技:仮想スクロール
データがさらに多い場合は、仮想スクロール(Virtual Scrolling)と組み合わせましょう。表示領域だけ描画し、1000 件受信してもカクつきにくくなります。
React なら react-window や react-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。fs、crypto、DB 接続など Node.js API が使えます。
Edge Runtime は V8 ベース(Chrome と同系)で、完全な Node.js 環境ではありません。起動は超高速(0〜5ms)ですが、使える API は限られます。
簡単な比較:
| 特性 | Node.js Runtime | Edge Runtime |
|---|---|---|
| 起動速度 | 100-500ms | 0-5ms |
| 利用可能 API | Node.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' })
}
シナリオ 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 が使えない
fs、path、child_process などは不可。コードにこれらがあると Edge 移行でエラーになります。
制限 2:DB 接続
従来の pg や mysql2 は 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 }
})
}
無効リクエストはエッジで遮断。中心サーバーまで届きません。
最適化効果の比較
| 指標 | 最適化前 | 最適化後 | 改善 |
|---|---|---|---|
| 初回アクセス応答時間 | 2800ms | 300ms(初回バッチ) | 89% ↓ |
| キャッシュヒット応答時間 | - | 50ms | 98% ↓ |
| JSON サイズ | 2.3MB | 180KB | 92% ↓ |
| ユーザー操作可能時間 | 2800ms | 300ms | 89% ↓ |
| サーバー負荷 | 100% | 10% | 90% ↓ |
「いつの時代のサイト?」という声は、なくなりました。
性能監視と継続的最適化
最適化で終わりではありません。継続監視しないと効果も問題も見えません。
重要指標
私が見ている指標は次の 4 つです。
-
応答時間分布(P50、P95、P99)
- P50(中央値):半数のユーザー体験
- P95:95% のユーザー体験
- P99:最遅 1%(異常の兆候になりやすい)
-
キャッシュヒット率
- 70% 未満なら戦略見直し
- 95% 超ならキャッシュが長すぎて鮮度不足の可能性
-
エラー率
- 最適化後に上がっていないか確認
- ストリーミングは途中失敗に注意
-
地域分布
- 地域別レイテンシ差
- 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)
}
継続最適化の提案
- キャッシュ戦略の定期見直し:業務が変われば戦略も変える
- A/B テスト:どちらが良いか不明なら試す
- 実データで調整:感覚ではなく監視データで判断
パフォーマンス最適化は継続プロセス。一度きりではありません。
まとめ
要点を整理します。
キャッシュ戦略:データ特性で選ぶ。静的は長キャッシュ、ユーザー向けは stale-while-revalidate、リアルタイムはキャッシュしない。更新後の無効化も忘れずに。
ストリーミング:データが大きい・計算が重いときの切り札。白画面で待たせず、早く内容を見せる。フロントは仮想スクロールと相性が良い。
Edge Functions:認証、地域判定、API プロキシなど軽量ロジック向き。複雑業務は Node.js Runtime と併用が正解。
最適化は一気に完璧を目指さなくて大丈夫。最も遅い API から 3 手法を試し、実測して調整——段階的に進めましょう。
私のブログ一覧 API は 3 秒から 300ms へ。ユーザー体験の改善は目に見えて大きかったです。遅い API を 1 つ選び、今日から試してみてください。質問はコメントで——一緒に進めましょう。
FAQ
Next.js API のキャッシュはいつ無効になりますか?
ストリーミングレスポンスはすべての API に向いていますか?
Edge Functions にはどんな制限がありますか?
キャッシュ戦略はどう選べばいいですか?
最適化後の効果はどう検証しますか?
6分で読めます · 公開日: 2026年1月5日 · 更新日: 2026年6月8日
Next.js 完全ガイド
検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。
前の記事
Next.js API Routes 完全ガイド:Route Handlers からエラー処理のベストプラクティスまで
Next.js API Routes 完全ガイド。Route Handlers の作成方法、リクエスト処理のテクニック、エラー処理のベストプラクティス、レスポンス形式の設計まで解説し、Next.js でバックエンド API を開発するための実践ガイド。
第 17 / 47 記事
次の記事
Next.js API 認証とセキュリティ:JWT からレート制限まで完全実践ガイド
JWT 認証、CORS 設定、レート制限、入力検証まで。Next.js API を本番環境で安全に運用するための完全ガイド。最新の脆弱性対策を含め、攻撃からアプリを守る実践コードと手順を解説。
第 19 / 47 記事
関連記事
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js App Router 入門ガイド:コア概念と基本操作を解説
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js 15 実践:週末で本番級ブログシステムを構築した方法
Next.js Middleware 実践ガイド:パスマッチ、Edge Runtime 制限とよくある落とし穴
コメント
GitHubアカウントでログインしてコメントできます