Next.js 15がリリース - Turbopack安定化とReact 19対応

2025.12.02

公式ドキュメント

Next.js 15の概要

Next.js 15は、React 19の正式サポートとTurbopackの安定化を実現したメジャーリリースです。開発体験とパフォーマンスの両面で大幅な改善が行われています。

flowchart TB
    subgraph Next15["Next.js 15 Highlights"]
        subgraph Turbo["Turbopack (Stable for Dev)"]
            T1["開発サーバー起動: 76% faster"]
            T2["Fast Refresh: 96% faster"]
            T3["初回コンパイル: 45% faster"]
        end

        subgraph React19["React 19 Support"]
            R1["Server Actions (Stable)"]
            R2["use() Hook"]
            R3["React Compiler (Experimental)"]
        end

        subgraph PPR["Partial Prerendering (Beta)"]
            P1["静的シェル + 動的コンテンツのストリーミング"]
        end

        subgraph Cache["New Caching Defaults"]
            C1["fetch: no-store (デフォルト)"]
            C2["Route Handlers: no-store (デフォルト)"]
        end
    end

Turbopackの安定化

Next.js 15では、開発サーバー用のTurbopackが安定版になりました。

# Turbopackで開発サーバー起動
next dev --turbo

# ビルド時はまだWebpack(Turbopackは開発中)
next build

パフォーマンス比較

大規模プロジェクト (5000モジュール) でのパフォーマンス比較:

項目WebpackTurbopack改善率
開発サーバー起動8.2秒2.0秒76% faster
Fast Refresh(1ファイル変更)800ms30ms96% faster
ルート初回コンパイル600ms330ms45% faster
メモリ使用量1.6GB800MB50% reduction

React 19サポート

Server Actions

// app/actions.ts
'use server';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // バリデーション
  if (!title || title.length < 3) {
    return { error: 'Title must be at least 3 characters' };
  }

  // データベース操作
  await db.posts.create({ title, content });

  // キャッシュの再検証
  revalidatePath('/posts');

  // リダイレクト
  redirect('/posts');
}

// 楽観的更新付きのアクション
export async function likePost(postId: string) {
  await db.posts.update({
    where: { id: postId },
    data: { likes: { increment: 1 } },
  });

  revalidatePath(`/posts/${postId}`);
}
// app/posts/new/page.tsx
import { createPost } from '../actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}

useActionState

'use client';

import { useActionState } from 'react';
import { createPost } from '../actions';

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null);

  return (
    <form action={formAction}>
      <input name="title" placeholder="Title" disabled={isPending} />
      <textarea name="content" placeholder="Content" disabled={isPending} />

      {state?.error && (
        <p className="text-red-500">{state.error}</p>
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

useOptimistic

'use client';

import { useOptimistic } from 'react';
import { likePost } from '../actions';

export function LikeButton({ postId, initialLikes }: Props) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    initialLikes,
    (state, _) => state + 1
  );

  async function handleLike() {
    addOptimisticLike(null);
    await likePost(postId);
  }

  return (
    <form action={handleLike}>
      <button type="submit">
        ❤️ {optimisticLikes}
      </button>
    </form>
  );
}

Partial Prerendering (PPR)

静的シェルと動的コンテンツを組み合わせた新しいレンダリング戦略です。

flowchart TB
    subgraph BuildTime["ビルド時"]
        subgraph StaticShell["Static Shell"]
            Header["Header (静的)"]
            subgraph Layout["Layout"]
                Content["Content (静的)"]
                Suspense["Suspense Fallback<br/>(Skeleton)"]
            end
        end
    end

    subgraph RequestTime["リクエスト時"]
        Step1["1. 静的シェルを即座に返却 (TTFB: 数ms)"]
        Step2["2. 動的部分をストリーミング"]
        Step3["3. Suspense境界が順次ハイドレート"]
        Step1 --> Step2 --> Step3
    end

    BuildTime --> RequestTime
// next.config.js
module.exports = {
  experimental: {
    ppr: true,
  },
};
// app/products/[id]/page.tsx
import { Suspense } from 'react';

// 静的に生成される部分
export default function ProductPage({ params }: Props) {
  return (
    <div>
      {/* 静的コンテンツ */}
      <ProductDetails id={params.id} />

      {/* 動的コンテンツ(ストリーミング) */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <PersonalizedRecommendations userId={getUserId()} />
      </Suspense>

      <Suspense fallback={<ReviewsSkeleton />}>
        <RecentReviews productId={params.id} />
      </Suspense>
    </div>
  );
}

// 動的データを使用するコンポーネント
async function PersonalizedRecommendations({ userId }: { userId: string }) {
  // この部分はリクエスト時に実行される
  const recommendations = await getRecommendations(userId);
  return <RecommendationList items={recommendations} />;
}

新しいキャッシュセマンティクス

Next.js 15では、キャッシュのデフォルト動作が変更されました。

// Next.js 14以前: デフォルトでキャッシュ
// Next.js 15: デフォルトでキャッシュなし

// fetch APIのデフォルト変更
async function getData() {
  // Next.js 14: cache: 'force-cache' がデフォルト
  // Next.js 15: cache: 'no-store' がデフォルト
  const res = await fetch('https://api.example.com/data');
  return res.json();
}

// キャッシュを有効にする場合は明示的に指定
async function getCachedData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache',
    next: { revalidate: 3600 }, // 1時間
  });
  return res.json();
}

Route Handlersのキャッシュ

// app/api/data/route.ts

// Next.js 14: GET は静的にキャッシュ
// Next.js 15: デフォルトでキャッシュなし

// キャッシュを有効にする
export const dynamic = 'force-static';
// または
export const revalidate = 3600;

export async function GET() {
  const data = await fetchData();
  return Response.json(data);
}

セキュリティ強化

Server Actionsのセキュリティ

// Server Actionsのエンドポイント保護
// next.config.js
module.exports = {
  experimental: {
    serverActions: {
      // 許可するオリジンを指定
      allowedOrigins: ['my-app.com', 'staging.my-app.com'],
    },
  },
};
// app/actions.ts
'use server';

import { headers } from 'next/headers';

export async function sensitiveAction() {
  // リファラーチェック
  const referer = headers().get('referer');
  if (!referer?.startsWith('https://my-app.com')) {
    throw new Error('Invalid origin');
  }

  // CSRF保護は自動的に行われる
  // ...
}

静的エクスポートの改善

// next.config.js
module.exports = {
  output: 'export',

  // 静的エクスポート時の画像最適化
  images: {
    unoptimized: true,
    // または外部ローダーを使用
    loader: 'custom',
    loaderFile: './image-loader.js',
  },
};
// image-loader.js
export default function customLoader({ src, width, quality }) {
  return `https://cdn.example.com/${src}?w=${width}&q=${quality || 75}`;
}

instrumentation.ts

アプリケーションのブートストラップ時にコードを実行できます。

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    // Node.jsランタイムでのみ実行
    await import('./monitoring/sentry');
  }

  if (process.env.NEXT_RUNTIME === 'edge') {
    // Edgeランタイムでのみ実行
    await import('./monitoring/edge-logger');
  }
}

export function onRequestError(error: Error, request: Request) {
  // エラーログの送信
  console.error('Request error:', error);
}

マイグレーション

# アップグレード
npm install next@15 react@19 react-dom@19

# 自動コードモッド
npx @next/codemod@latest upgrade latest

主な破壊的変更

変更点Next.js 14Next.js 15
fetch デフォルトcache: ‘force-cache’cache: ‘no-store’
Route Handler静的キャッシュキャッシュなし
React バージョンReact 18React 19
Node.js 最小要件18.17.018.18.0

参考リンク

← 一覧に戻る