React Server Components完全ガイド - Next.js App Routerで実践
React Server Componentsの概念から実装まで、Next.jsのApp Routerを使った実践的な解説。Client ComponentsとServer Componentsの使い分けやパフォーマンス最適化のテクニックを詳しく解説します。
Next.js 15の革新的な新機能を完全解説。Turbopack正式版、React 19対応、部分的プリレンダリング、新しいキャッシュ戦略など、実践的なコード例とともに最新機能を網羅します。
Next.js 15 は、2024 年 10 月にリリースされ、2025 年現在最も注目されている react フレームワークの最新版です。 Turbopack の正式採用、react 19 との統合、革新的なキャッシュ戦略など、開発体験とパフォーマンスを 大幅に向上させる機能が満載。本記事では、実践的なコード例とともに徹底解説します。
新しいルーティングシステムの登場
サーバーサイド処理の簡素化
開発速度とレンダリング性能の革新
さらなるエッジ対応強化
機能 | 概要 | インパクト | 移行難易度 |
---|---|---|---|
Turbopack | Rustベースの高速バンドラー | 開発速度10倍 | 簡単 |
React 19対応 | 最新React機能のフル活用 | DX向上 | 中 |
部分的プリレンダリング | 静的・動的の最適な組み合わせ | 性能向上 | 中 |
キャッシュ戦略刷新 | より細かい制御が可能に | 柔軟性向上 | 高 |
Instrumentationフック | アプリケーション監視強化 | 運用改善 | 簡単 |
Turbopack は、Vercel が開発した Rust 製の次世代 javascript バンドラーです。 Webpack の後継として設計され、開発環境での圧倒的な速度向上を実現します。
// next.config.js
module.exports = {
experimental: {
turbo: {
// Turbopackの詳細設定
rules: {
'*.svg': {
loaders: ['@svgr/webpack'],
as: '*.js',
},
},
},
},
}
# Turbopackを使用した開発サーバー起動
next dev --turbo
# または package.json に設定
{
"scripts": {
"dev": "next dev --turbo"
}
}
Next.js 15 は、react 19 の革新的な機能を完全サポートしています。
// React Compilerが自動的にメモ化
// useMemoやuseCallbackが不要に
function TodoList({ todos, filter }) {
// 自動的に最適化される
const visibleTodos = todos.filter(todo =>
todo.text.includes(filter)
);
return (
<ul>
{visibleTodos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
}
// 非同期データの直接的な扱い
import { use } from 'react';
function UserProfile({ userId }) {
// Promiseを直接扱える
const user = use(fetchUser(userId));
return <h1>{user.name}</h1>;
}
// app/actions.js
'use server';
export async function updateUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
await db.user.update({
where: { email },
data: { name }
});
revalidatePath('/profile');
}
// app/profile/page.jsx
import { updateUser } from '../actions';
export default function ProfilePage() {
return (
<form action={updateUser}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit">更新</button>
</form>
);
}
部分的プリレンダリング(Partial Pre-Rendering)は、静的レンダリングと動的レンダリングを コンポーネントレベルで組み合わせる革新的な技術です。
チャートを読み込み中...
// app/product/[id]/page.jsx
import { Suspense } from 'react';
// 静的にプリレンダリングされる部分
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* 動的にストリーミングされる部分 */}
<Suspense fallback={<PriceSkeleton />}>
<ProductPrice productId={params.id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews productId={params.id} />
</Suspense>
</div>
);
}
// 動的コンポーネント
async function ProductPrice({ productId }) {
// リアルタイムの価格情報を取得
const price = await getRealtimePrice(productId);
return <div className="price">{price}</div>;
}
// next.config.js
module.exports = {
experimental: {
ppr: true, // PPRを有効化
},
}
Next.js 15 では、4 つのキャッシュレイヤーが存在します:
キャッシュ種別 | 対象 | デフォルト | 制御方法 |
---|---|---|---|
Request Memoization | 同一リクエスト内 | 有効 | React.cache() |
Data Cache | fetchリクエスト | 有効 | fetch options |
Full Route Cache | 静的ルート | 有効 | revalidate |
Router Cache | クライアント側 | 有効 | router.refresh() |
// app/api/data/route.js
import { unstable_cache } from 'next/cache';
// カスタムキャッシュの作成
const getCachedData = unstable_cache(
async (id) => {
const data = await db.query(`SELECT * FROM items WHERE id = ?`, [id]);
return data;
},
['items'], // キャッシュキー
{
revalidate: 3600, // 1時間
tags: ['items'], // キャッシュタグ
}
);
// データフェッチング with キャッシュ制御
export async function GET(request) {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
const data = await getCachedData(id);
return Response.json(data);
}
// app/actions.js
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
export async function updateItem(itemId, data) {
// データベース更新
await db.items.update({
where: { id: itemId },
data
});
// 特定のタグのキャッシュを無効化
revalidateTag('items');
revalidateTag(`item-${itemId}`);
// 特定のパスのキャッシュを無効化
revalidatePath(`/items/${itemId}`);
revalidatePath('/items', 'page');
}
// app/layout.jsx - 並列ルートの定義
export default function Layout({ children, modal }) {
return (
<>
{children}
{modal}
</>
);
}
// app/@modal/(.)photos/[id]/page.jsx - インターセプトルート
export default function PhotoModal({ params }) {
return (
<div className="modal">
<Photo id={params.id} />
</div>
);
}
// app/api/stream/route.js - ストリーミングレスポンス
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
controller.enqueue(
encoder.encode(`data: Message ${i}\n\n`)
);
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
import Image from 'next/image';
export default function OptimizedGallery() {
return (
<div>
{/* 新しいplaceholder機能 */}
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
priority
quality={85}
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
);
}
// app/layout.jsx
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
});
export default function RootLayout({ children }) {
return (
<html lang="ja" className={`${inter.variable} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
);
}
// next.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
webpack: (config, { isServer }) => {
if (process.env.ANALYZE === 'true') {
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: isServer
? '../analyze/server.html'
: './analyze/client.html',
})
);
}
return config;
},
};
// app/products/[id]/page.jsx
import { Suspense } from 'react';
import { notFound } from 'next/navigation';
import { getProduct } from '@/lib/products';
import { ProductImages, ProductInfo, ProductReviews } from './components';
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
if (!product) return {};
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.image],
},
};
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<div className="container">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<ProductImages images={product.images} />
<div>
<ProductInfo product={product} />
<Suspense fallback={<div>価格を読み込み中...</div>}>
<ProductPrice productId={params.id} />
</Suspense>
<AddToCartButton productId={params.id} />
</div>
</div>
<Suspense fallback={<div>レビューを読み込み中...</div>}>
<ProductReviews productId={params.id} />
</Suspense>
</div>
);
}
// app/products/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';
export async function addToCart(productId, quantity = 1) {
const session = await auth();
if (!session) {
throw new Error('認証が必要です');
}
try {
await db.cart.upsert({
where: {
userId_productId: {
userId: session.user.id,
productId,
},
},
update: {
quantity: { increment: quantity },
},
create: {
userId: session.user.id,
productId,
quantity,
},
});
revalidatePath('/cart');
return { success: true };
} catch (error) {
return { error: error.message };
}
}
export async function addReview(productId, formData) {
const session = await auth();
if (!session) {
throw new Error('認証が必要です');
}
const rating = parseInt(formData.get('rating'));
const comment = formData.get('comment');
await db.review.create({
data: {
productId,
userId: session.user.id,
rating,
comment,
},
});
revalidatePath(`/products/${productId}`);
}
// app/products/[id]/components.jsx
'use client';
import { useState, useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { addToCart } from '../actions';
export function AddToCartButton({ productId }) {
const [isPending, startTransition] = useTransition();
const [showSuccess, setShowSuccess] = useState(false);
const router = useRouter();
async function handleAddToCart() {
startTransition(async () => {
const result = await addToCart(productId);
if (result.success) {
setShowSuccess(true);
setTimeout(() => setShowSuccess(false), 3000);
router.refresh();
}
});
}
return (
<div>
<button
onClick={handleAddToCart}
disabled={isPending}
className="btn btn-primary w-full"
>
{isPending ? '追加中...' : 'カートに追加'}
</button>
{showSuccess && (
<div className="mt-2 text-green-600">
✓ カートに追加しました
</div>
)}
</div>
);
}
export function ProductPrice({ productId }) {
const [price, setPrice] = useState(null);
useEffect(() => {
// リアルタイム価格の取得
const eventSource = new EventSource(`/api/prices/${productId}`);
eventSource.onmessage = (event) => {
setPrice(JSON.parse(event.data));
};
return () => eventSource.close();
}, [productId]);
if (!price) return <PriceSkeleton />;
return (
<div className="price-display">
<span className="text-3xl font-bold">¥{price.current}</span>
{price.original > price.current && (
<span className="text-gray-500 line-through ml-2">
¥{price.original}
</span>
)}
</div>
);
}
// app/components/WebVitalsReporter.jsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitalsReporter() {
useReportWebVitals((metric) => {
// 分析サービスに送信
if (typeof window !== 'undefined') {
window.gtag?.('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_label: metric.id,
non_interaction: true,
});
}
// コンソールに出力(開発時)
if (process.env.NODE_ENV === 'development') {
console.log(metric);
}
});
return null;
}
Next.js 15 は、モダンな web 開発における多くの課題を解決する強力な機能を提供しています。
既存プロジェクトの場合
新規プロジェクトの場合
Next.js 15 は、フルスタック react アプリケーションの新しいスタンダードを確立しました。 エッジコンピューティング、ai 統合、さらなるパフォーマンス最適化など、 今後もエキサイティングな進化が期待されます。
この記事は2025年6月時点の情報に基づいています。Next.jsは活発に開発されているため、最新の公式ドキュメントも参照してください。