Back

Next.js 16マイグレーション完全ガイド:Turbopack、proxy.ts、Cache Components、全Breaking Change解説

CIパイプラインがnext^16.0.0に上げた途端に壊れました。エラーログは400行、ミドルウェアの半分が動かない、見たこともないproxy.tsというファイルコンベンションが登場している。Next.js 16アップグレードの世界へようこそ。

Next.js 16は、App Router登場以降最大のアーキテクチャ変更。Webpackが消えてTurbopack一本、middleware.tsproxy.tsに変わり、キャッシュはuse cacheに統一された。コンテナ環境で動かしているなら、メモリ周りの挙動変更も把握しておかないと本番で痛い目を見る。

この記事では全breaking changeを一つずつ潰し、変更理由、codemodコマンド、手動マイグレーションパスまでカバーする。小規模サイトでも200ルートのエンタープライズアプリでも、この記事一本で足りる。

何が変わって、なぜ変わったのか

マイグレーション手順に入る前に、Next.js 16が何をどう変えたかの全体像を把握しましょう:

┌─────────────────────────────────────────────────────────────┐
│                    Next.js 16 アーキテクチャ                  │
│                                                              │
│  ┌──────────────┐  ┌──────────────┐  ┌─────────────────┐   │
│  │  Turbopack    │  │  proxy.ts    │  │  Cache           │   │
│  │  (唯一の      │  │  (middleware │  │  Components      │   │
│  │   バンドラー) │  │   を置換)    │  │  ('use cache')   │   │
│  └──────┬───────┘  └──────┬───────┘  └────────┬────────┘   │
│         │                  │                    │             │
│  ┌──────┴───────┐  ┌──────┴───────┐  ┌────────┴────────┐   │
│  │  10倍速い    │  │  明確な      │  │  宣言的          │   │
│  │  HMR & ビルド│  │  ネットワーク│  │  キャッシュ +    │   │
│  │              │  │  境界        │  │  細かいTTL制御   │   │
│  └──────────────┘  └──────────────┘  └──────────────────┘   │
│                                                              │
│  ┌──────────────┐  ┌──────────────┐                         │
│  │  非同期       │  │  PPR         │                         │
│  │  Request API │  │  (デフォルト)│                         │
│  │  (params,    │  │              │                         │
│  │   cookies,   │  │              │                         │
│  │   headers)   │  │              │                         │
│  └──────────────┘  └──────────────┘                         │
└─────────────────────────────────────────────────────────────┘
変更理由
TurbopackがWebpackを置換Webpackのアーキテクチャではモジュール500個を超えるとサブセカンドHMRがスケールしなかった。Turbopackのインクリメンタル計算エンジンは10K+モジュールのグラフでも一貫して200ms以内の更新を維持する。
proxy.tsmiddleware.tsを置換middlewareがリクエストレベルのロジック(認証、リダイレクト)とネットワークプロキシ動作(リライト、ヘッダーインジェクション)を混在させていた。proxy.tsでEdge実行コードとサーバー実行コードの境界が明確になった。
非同期Request APIparamssearchParamscookies()headers()がすべてasync化。ストリーミングが改善され、RSCレンダリングでの暗黙的ブロッキングがなくなった。
Cache Components (use cache)従来のrevalidateunstable_cache、ネストしたcache()パターンが予測不能なキャッシュ階層を作っていた。use cacheで単一の宣言的かつ合成可能なプリミティブに統一。
PPRがデフォルトPartial Prerenderingがデフォルトレンダリング戦略に。静的シェルにストリーミングで動的コンテンツを組み合わせ、SSRかSSGかで悩む必要がなくなった。

ステップ0:始める前に

1. 現在のメトリクスを記録する

コードをいじる前に、今のパフォーマンスを記録しておきましょう:

# パフォーマンスベースライン npx next build 2>&1 | tee build-before.log npx @next/bundle-analyzer # ランタイムメトリクス lighthouse https://your-app.com --output json --output-path baseline.json

2. Node.js互換性の確認

Next.js 16はNode.js 20.x以上が必要です。Node 18はサポート外。

node -v # v20.0.0以上であること

3. アップグレードコマンドの実行

npx @next/codemod@latest upgrade

自動マイグレーションのかなりの部分を処理してくれます。ただし全部はカバーしきれない。残りはこのガイドで解説します。

ステップ1:Turbopack — Webpackはもうデフォルトじゃない

一番目立つ変更。devもbuildもTurbopackがデフォルトになった。next.config.tswebpackキーはdeprecatedだが、どうしても互換性のないローダーがあるならnext dev --webpacknext build --webpackで一時的に通る。あくまで繋ぎなので、最終的にはこれを外すのがゴール。

何が壊れるか

next.config.tsにこういうのがあると全部失敗します:

// ❌ Next.js 16では動作しない module.exports = { webpack: (config) => { config.resolve.alias['@'] = path.resolve(__dirname, 'src'); config.module.rules.push({ test: /\.svg$/, use: ['@svgr/webpack'], }); return config; }, };

マイグレーション方法

エイリアス: tsconfig.jsonのビルトインpathsを使いましょう。Turbopackがネイティブで認識します:

// tsconfig.json { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } }

SVGインポート: @svgr/webpackの代わりに@svgr/turbopackを使用:

npm install @svgr/turbopack
// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { turbopack: { rules: { '*.svg': { loaders: ['@svgr/turbopack'], as: '*.js', }, }, }, }; export default nextConfig;

その他のカスタムローダー: Turbopack互換バージョンがあるか確認しましょう。人気のローダー(CSSモジュール、SASS、画像最適化)はTurbopackにビルトインです。それ以外はturbopack.rules設定がエスケープハッチになります。

検証

# 開発 npx next dev # エラーなく起動すればTurbopack正常動作 # プロダクションビルド npx next build

ビルド中に以前なかったModule not foundエラーが出たら、ほぼ確実にローダー互換性の問題です。使用中のローダーについてTurbopack互換性テーブルを確認してください。

ステップ2:middleware.ts → proxy.ts

最も混乱を招く変更がこれ。従来のmiddleware.tsが2つの概念レイヤーに分離されました:

  1. proxy.ts — ネットワークレベルの操作(URLリライト、ヘッダーインジェクション、ジオロケーションルーティング)。デフォルトでNode.jsランタイムで実行(Edgeがデフォルトだった旧middlewareとは異なる)。
  2. ルートハンドラー内のサーバーサイドロジック — 認証チェック、セッション検証、ビジネスロジックは実際のルートハンドラー、レイアウト、Server Actionsに置くべき。

典型的なmiddleware.tsはこうだった

// ❌ 旧:middleware.ts(Next.js 15) import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { // 認証チェック const token = request.cookies.get('session-token'); if (!token && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); } // ロケール検出 const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en'; const response = NextResponse.next(); response.headers.set('x-locale', locale); // A/Bテストルーティング const bucket = Math.random() > 0.5 ? 'a' : 'b'; response.headers.set('x-ab-bucket', bucket); return response; } export const config = { matcher: ['/((?!api|_next/static|favicon.ico).*)'], };

新しいproxy.ts

// ✅ 新:proxy.ts(Next.js 16) import type { NextRequest } from 'next/server'; export function proxy(request: NextRequest) { const url = request.nextUrl.clone(); // ロケール検出(ネットワークレベルの関心事) const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en'; // A/Bテストルーティング(ネットワークレベルの関心事) const bucket = Math.random() > 0.5 ? 'a' : 'b'; return { headers: { 'x-locale': locale, 'x-ab-bucket': bucket, }, }; } export const config = { matcher: ['/((?!api|_next/static|favicon.ico).*)'], };

認証ロジックはどこへ行く?

認証チェックは、保護するルートのレイアウトやルートハンドラーに移動します:

// app/dashboard/layout.tsx import { redirect } from 'next/navigation'; import { getSession } from '@/lib/auth'; export default async function DashboardLayout({ children, }: { children: React.ReactNode; }) { const session = await getSession(); if (!session) { redirect('/login'); } return <>{children}</>; }

実際、この方が設計として正しい。認証ロジックが守るルートのすぐ横にあるので、「どこで弾かれてるの?」と迷うことがなくなる。グローバルなmiddlewareに隠していた頃とは比較にならない。

Codemod

npx @next/codemod@latest middleware-to-proxy

シンプルなヘッダー/リライトパターンは処理してくれますが、認証ロジックの自動移動はしません。出力を必ず確認しましょう。

ステップ3:非同期Request API

すべてのダイナミックリクエストAPIが厳密にasyncになりました。ファイル数ベースで最もインパクトの大きい変更です。params、search params、cookies、headersにアクセスするすべてのページ、レイアウト、ルートハンドラーを修正する必要があります。

Before(Next.js 15)

// ❌ 同期アクセスはもう動かない export default function Page({ params, searchParams, }: { params: { slug: string }; searchParams: { q?: string }; }) { const title = params.slug; const query = searchParams.q; return <div>{title} - {query}</div>; }

After(Next.js 16)

// ✅ すべてのRequest APIがasync export default async function Page({ params, searchParams, }: { params: Promise<{ slug: string }>; searchParams: Promise<{ q?: string }>; }) { const { slug } = await params; const { q } = await searchParams; return <div>{slug} - {q}</div>; }

cookies()とheaders()

// ❌ Before import { cookies, headers } from 'next/headers'; export default function Page() { const cookieStore = cookies(); const headerList = headers(); // ... } // ✅ After import { cookies, headers } from 'next/headers'; export default async function Page() { const cookieStore = await cookies(); const headerList = await headers(); // ... }

Codemod

npx @next/codemod@latest async-request-apis

このcodemodは成功率が高い(~90%)。関数にasyncを追加し、awaitでラップし、型シグネチャも更新してくれます。結果は必ずチェックしましょう。paramsを別のユーティリティ関数にパススルーしているケースで漏れることがあります。

ステップ4:Cache Componentsとuse cacheディレクティブ

一番大きな概念の変化。キャッシュ設定するとき、revalidate使うか、unstable_cache使うか、fetchのオプション使うか、毎回迷ってたでしょ。あのカオスがuse cache一本に整理された。

旧世界(混乱)

// ❌ Next.js 15:複数のキャッシュメカニズムが重複 export const revalidate = 3600; // ページレベルの再検証 async function getData() { const res = await fetch('https://api.example.com/data', { next: { revalidate: 60, tags: ['data'] }, }); return res.json(); } // さらに:unstable_cache、cache()、generateStaticParams...

新世界(宣言的)

まず、設定でCache Componentsを有効化します:

// next.config.ts const nextConfig: NextConfig = { cacheComponents: true, // 'use cache' の使用に必須 };

それからディレクティブを使います:

// ✅ Next.js 16:単一の'use cache'ディレクティブ import { cacheLife, cacheTag } from 'next/cache'; // 関数レベルキャッシュ async function getData() { 'use cache'; cacheLife('hours'); cacheTag('data'); const res = await fetch('https://api.example.com/data'); return res.json(); } // コンポーネントレベルキャッシュ async function ExpensiveWidget() { 'use cache'; cacheLife('days'); cacheTag('widget'); const data = await getData(); return <div>{data.title}</div>; } // ページレベルキャッシュ export default async function Page() { 'use cache'; cacheLife('minutes'); return ( <main> <ExpensiveWidget /> <DynamicContent /> </main> ); }

Cache Lifeプリセット

Next.js 16にはビルトインのキャッシュ寿命プリセットが含まれています:

プリセットStaleRevalidateExpire
'seconds'0秒1秒60秒
'minutes'5分1分1時間
'hours'5分1時間24時間
'days'5分1日14日
'weeks'5分1週間30日
'max'5分30日365日

カスタムプロファイルも定義可能:

// next.config.ts const nextConfig: NextConfig = { cacheLife: { product: { stale: 300, // 5分 revalidate: 3600, // 1時間 expire: 86400, // 1日 }, }, };
// こう使う: async function getProduct(id: string) { 'use cache'; cacheLife('product'); cacheTag(`product-${id}`); return db.products.findById(id); }

再検証(Revalidation)

タグベースの再検証は同じ仕組みですが、どの粒度でもタグ付けできるようになったので、より強力になりました:

// app/api/revalidate/route.ts import { revalidateTag } from 'next/cache'; export async function POST(request: Request) { const { tag } = await request.json(); revalidateTag(tag); return Response.json({ revalidated: true }); }

マイグレーション戦略

  1. ページからexport const revalidate = ...を全部削除
  2. fetch(..., { next: { revalidate } })use cache + cacheLifeに置換
  3. unstable_cache()呼び出しをuse cache関数に置換
  4. ターゲット再検証が必要な箇所にcacheTag()を追加

ステップ5:PPR(Partial Prerendering)がデフォルトに

PPRがデフォルトのレンダリング戦略になりました。すべてのページが自動的に即座に配信される静的シェルを持ち、動的コンテンツはSuspenseバウンダリ経由でストリーミングされます。

実際にどう動くか

// Next.js 16でこのページは自動的にPPRを使用 export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return ( <main> {/* 静的シェル — CDNから配信 */} <Header /> <ProductInfo id={id} /> {/* 動的コンテンツ — ストリーミング */} <Suspense fallback={<PriceSkeleton />}> <DynamicPrice id={id} /> </Suspense> <Suspense fallback={<ReviewsSkeleton />}> <UserReviews id={id} /> </Suspense> </main> ); }

ポイントはシンプル。use cacheが付いたコンポーネント = 静的シェル。 cookiesやheadersなど動的データを使うコンポーネント = ストリーミングで後から埋まる穴。これだけ覚えればPPRは理解できた。

PPRを無効にしたい場合

// next.config.ts const nextConfig: NextConfig = { experimental: { ppr: false, // PPRをグローバル無効化 }, };

ルートごとの無効化も可能:

// app/legacy-page/page.tsx export const dynamic = 'force-dynamic'; // このページはPPRを使わない

ステップ6:コンテナのメモリ最適化

地味にハマるやつ。Next.js 16のRSCレンダリングはv15より明らかにメモリを食う。コンテナのメモリ制限がタイトだとOOMでPodが落ちる。

問題

KubernetesやDockerデプロイでメモリ制限(例:Pod当たり512MB)を設定している場合、Next.js 15では起きなかったOOM killが発生する可能性があります。原因はTurbopackのインメモリモジュールグラフとRSCレンダリングエンジンがより多くの中間状態を保持すること。

解決策

// next.config.ts const nextConfig: NextConfig = { turbopack: { memoryLimit: 256 * 1024 * 1024, // 256MB }, experimental: { incrementalCacheHandlerPath: './cache-handler.mjs', }, };
// cache-handler.mjs import { CacheHandler } from '@next/cache-handler-redis'; export default class CustomCacheHandler extends CacheHandler { constructor(options) { super({ ...options, redis: { url: process.env.REDIS_URL, }, inMemoryCacheEnabled: false, }); } }

コンテナリソース推奨値

アプリ規模推奨メモリ推奨CPU
小規模(<50ルート)512MB0.5 vCPU
中規模(50-200ルート)1GB1 vCPU
大規模(200+ルート)2GB2 vCPU

モニタリング:

# ビルド中のメモリ使用量を監視 docker stats --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}" # ランタイムNode.jsメモリプロファイリング NODE_OPTIONS="--max-old-space-size=1024 --heapsnapshot-near-heap-limit=3" npm start

ステップ7:next.config.tsの変更

いくつかの設定オプションがリネームまたは構造変更されました:

// next.config.ts — Next.js 16完全設定 import type { NextConfig } from 'next'; const nextConfig: NextConfig = { turbopack: { rules: { '*.svg': { loaders: ['@svgr/turbopack'], as: '*.js', }, }, resolveAlias: { 'legacy-lib': './src/lib/legacy-adapter', }, }, cacheLife: { product: { stale: 300, revalidate: 3600, expire: 86400 }, blog: { stale: 60, revalidate: 900, expire: 86400 }, }, images: { remotePatterns: [ { protocol: 'https', hostname: '**.example.com' }, ], }, async redirects() { return [ { source: '/old-path', destination: '/new-path', permanent: true }, ]; }, }; export default nextConfig;

削除されたオプション

これらのnext.config.tsオプションはもう存在しません:

// ❌ Next.js 16ですべて削除 { webpack: () => {}, // turbopack.rules を使用 swcMinify: true, // 常にON(Turbopack) experimental: { appDir: true, // v14から常にON serverActions: true, // v15から常にON typedRoutes: true, // 常にON }, }

マイグレーションチェックリスト完全版

codemod実行後、このチェックリストを確認していきましょう:

インフラ

  • Node.js >= 20.x インストール済み
  • next^16.0.0にアップグレード
  • reactreact-dom^19.2.0にアップグレード
  • すべての@next/*パッケージのバージョンを揃える
  • コンテナメモリ制限を確認(512MB未満なら増加)

バンドラー

  • next.config.tsからすべてのwebpack設定を削除
  • カスタムローダーをturbopack.rulesに移行
  • tsconfig.jsonのpathsがTurbopackで動作確認
  • npx next buildがエラーなく完了

ルーティング

  • middleware.tsproxy.ts移行(ネットワーク関心事のみ)
  • 認証ロジックをmiddlewareからlayout/route handlerに移動
  • proxy.tsのmatcherパターンを確認

データフェッチ

  • すべてのparamssearchParamsPromise<T> + await
  • すべてのcookies()headers()呼び出しにawait追加
  • export const revalidateuse cache + cacheLifeに変換
  • next.config.tscacheComponents: trueを有効化
  • unstable_cacheuse cache関数に置換
  • ターゲット再検証のためcacheTag()追加

レンダリング

  • SuspenseバウンダリでPPR動作を確認
  • 動的コンテンツ用スケルトンコンポーネント追加
  • PPR不要な箇所でdynamic = 'force-dynamic'テスト

プロダクション

  • ビルド時間ベンチマーク(Turbopackで改善を確認)
  • 本番トラフィックパターンでロードテスト
  • コンテナメモリ使用量を24時間モニタリング
  • プロダクションキャッシュヒット率を確認

実際のマイグレーションタイムライン

さまざまな規模のチームでの本番マイグレーション実績に基づく:

アプリ規模Codemodカバレッジ手動作業合計期間
小規模(<50ルート)~85%1-2日3-4日
中規模(50-200ルート)~75%3-5日1-2週間
大規模(200+ルート)~60%1-2週間3-4週間

最も時間を食うのは:

  1. カスタムWebpackローダー → Turbopack互換品探し
  2. 複雑なmiddlewareロジック → proxy + layout認証への分解
  3. キャッシュ戦略の再設計 → 旧revalidateパターンからuse cacheへのマッピング

マイグレーション後に期待できること

クリーンなマイグレーション後、チームからよく報告される結果:

  • 開発サーバー起動:60-80%高速化(Turbopack vs Webpack)
  • HMR更新:5-10倍高速化(一貫して200ms以内)
  • プロダクションビルド:20-40%高速化
  • TTFB:PPRで30-50%改善(静的シェル即時配信)
  • メモリ使用量:同等かやや増加(チューニング必要)

TurbopackとPPRのパフォーマンス向上だけで、マイグレーションの苦労は十分元が取れる。proxy.tsuse cacheでコードが綺麗になるのは、アプリが大きくなるほど効いてくるボーナス。

Next.js 16はオピニオンが強い。良いパターンを強制するが、先行投資としてのマイグレーション作業が必要。codemodで機械的な部分は処理できる。アーキテクチャの理解 — proxy分離、宣言的キャッシュ、PPR — は自分で消化するしかない。この記事で両方揃えた。アップグレードしよう。

Next.jsNext.js 16マイグレーションTurbopackReactTypeScriptWeb開発フロントエンドproxycache componentsPPR

関連ツールを見る

Pockitの無料開発者ツールを試してみましょう