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モジュール) でのパフォーマンス比較:
| 項目 | Webpack | Turbopack | 改善率 |
|---|---|---|---|
| 開発サーバー起動 | 8.2秒 | 2.0秒 | 76% faster |
| Fast Refresh(1ファイル変更) | 800ms | 30ms | 96% faster |
| ルート初回コンパイル | 600ms | 330ms | 45% faster |
| メモリ使用量 | 1.6GB | 800MB | 50% 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 14 | Next.js 15 |
|---|---|---|
| fetch デフォルト | cache: ‘force-cache’ | cache: ‘no-store’ |
| Route Handler | 静的キャッシュ | キャッシュなし |
| React バージョン | React 18 | React 19 |
| Node.js 最小要件 | 18.17.0 | 18.18.0 |