Next.js 15 Server Components と Client Components の使い分け完全ガイド【パフォーマンス最適化】
Next.js 15のServer ComponentsとClient Componentsの正確な使い分けを解説。パフォーマンス最適化のベストプラクティスと実装例を紹介します。
Next.js 15では、React Server Components(RSC)がデフォルトで採用され、Server ComponentsとClient Componentsの使い分けがパフォーマンス最適化の核心になっています。しかし、どのコンポーネントをサーバーで実行し、どれをクライアントで実行すべきかの判断は、多くの開発者にとって混乱の原因です。
この記事では、Next.js 15におけるServer ComponentsとClient Componentsの技術的な違い、使い分けの判断基準、そしてパフォーマンス最適化のベストプラクティスを、実装例とともに解説します。
Server Components と Client Components の技術的な違い
Next.js 15のApp Routerでは、すべてのコンポーネントがデフォルトでServer Componentsとして扱われます。Client Componentsを使用する場合のみ、ファイルの先頭に'use client'ディレクティブを記述します。
レンダリングの実行環境
Server Componentsはサーバー上でレンダリングされ、クライアントにはHTMLとしてシリアライズされた結果のみが送信されます。これにより、以下のメリットがあります:
- JavaScriptバンドルサイズの削減(コンポーネントコードがクライアントに送信されない)
- データベースや内部APIへの直接アクセス
- 機密情報(APIキー等)の安全な扱い
- サーバーリソースへの効率的なアクセス
Client Componentsはクライアント側でレンダリングされ、JavaScriptコードがブラウザに送信されます:
- インタラクティブな機能(
useState,useEffect等のReactフック) - ブラウザAPIへのアクセス(
window,localStorage等) - イベントリスナーの使用
- カスタムフックの利用
データフローの違い
次の図は、Server ComponentsとClient Componentsのデータフローを示しています:
flowchart TD
A["クライアント: ページリクエスト"] --> B["サーバー: Server Component 実行"]
B --> C["サーバー: データベースクエリ"]
C --> D["サーバー: RSC Payload 生成"]
D --> E["クライアント: HTML + RSC Payload 受信"]
E --> F["クライアント: Client Component ハイドレーション"]
F --> G["クライアント: インタラクティブUI"]
Server ComponentsはサーバーでレンダリングされてRSC Payloadとして送信され、Client Componentsは後からハイドレーションされます
使い分けの判断基準
以下の表は、Server ComponentsとClient Componentsの使い分けの明確な基準です:
| 機能・要件 | Server Components | Client Components |
|---|---|---|
| データフェッチ(DB・内部API) | ✅ 推奨 | ❌ |
| バックエンドリソースへの直接アクセス | ✅ 推奨 | ❌ |
| 機密情報の扱い(APIキー等) | ✅ 推奨 | ❌ 危険 |
| 静的コンテンツのレンダリング | ✅ 推奨 | ⚠️ 可能だが非効率 |
| インタラクティブ性(state, イベント) | ❌ 不可 | ✅ 必須 |
| Reactフック(useState, useEffect等) | ❌ 不可 | ✅ 必須 |
| ブラウザAPI(window, localStorage等) | ❌ 不可 | ✅ 必須 |
| カスタムフック | ❌ 不可 | ✅ 必須 |
| サードパーティライブラリ(Reactフック使用) | ❌ 不可 | ✅ 必須 |
実装例:認証フォームのコンポーネント分割
以下は、Server ComponentsとClient Componentsを適切に組み合わせた認証フォームの例です:
// app/login/page.tsx (Server Component)
import { LoginForm } from './LoginForm'
import { getServerSession } from '@/lib/auth'
import { redirect } from 'next/navigation'
export default async function LoginPage() {
// サーバー側でセッション確認
const session = await getServerSession()
if (session) redirect('/dashboard')
// 静的コンテンツはServer Componentで
return (
<div className="login-container">
<h1>ログイン</h1>
<p>アカウントにログインしてください</p>
{/* インタラクティブな部分のみClient Component */}
<LoginForm />
</div>
)
}
// app/login/LoginForm.tsx (Client Component)
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
export function LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const router = useRouter()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
if (res.ok) {
router.push('/dashboard')
router.refresh() // Server Componentsを再レンダリング
} else {
setError('ログインに失敗しました')
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="メールアドレス"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="パスワード"
required
/>
{error && <p className="error">{error}</p>}
<button type="submit">ログイン</button>
</form>
)
}
この例では、ページ全体はServer Componentとして実装し、セッション確認と静的コンテンツのレンダリングを担当します。フォームのインタラクティブな部分のみをClient Componentとして分離することで、クライアントに送信されるJavaScriptを最小限に抑えています。
パフォーマンス最適化のベストプラクティス
1. Client Componentsを「葉」に配置する
コンポーネントツリーの構造を設計する際は、Client Componentsをできるだけツリーの「葉」(末端)に配置します。これにより、親コンポーネントをServer Componentとして保ち、JavaScriptバンドルサイズを削減できます。
❌ 非推奨: 親をClient Componentに
'use client'
import { useState } from 'react'
export default function ProductPage({ product }) {
const [quantity, setQuantity] = useState(1)
// ページ全体がClient Componentになり、すべてのコードがクライアントに送信される
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<img src={product.image} alt={product.name} />
<div>
<label>数量:</label>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
/>
</div>
</div>
)
}
✅ 推奨: インタラクティブ部分のみClient Component化
// app/products/[id]/page.tsx (Server Component)
import { QuantitySelector } from './QuantitySelector'
export default async function ProductPage({ params }) {
const product = await fetchProduct(params.id)
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<img src={product.image} alt={product.name} />
{/* インタラクティブ部分のみClient Component */}
<QuantitySelector productId={product.id} />
</div>
)
}
// app/products/[id]/QuantitySelector.tsx (Client Component)
'use client'
import { useState } from 'react'
export function QuantitySelector({ productId }: { productId: string }) {
const [quantity, setQuantity] = useState(1)
return (
<div>
<label>数量:</label>
<input
type="number"
value={quantity}
onChange={(e) => setQuantity(Number(e.target.value))}
/>
</div>
)
}
2. Server ComponentsからClient Componentsへのデータ渡し
Server ComponentsからClient Componentsにデータを渡す際は、シリアライズ可能なデータ(JSON形式)のみを渡せます。関数やクラスインスタンスは渡せません。
// app/dashboard/page.tsx (Server Component)
import { UserProfile } from './UserProfile'
export default async function Dashboard() {
const userData = await fetchUserData() // サーバーでデータ取得
// シリアライズ可能なデータのみを渡す
return (
<UserProfile
name={userData.name}
email={userData.email}
joinedAt={userData.joinedAt.toISOString()} // Date → string
/>
)
}
// app/dashboard/UserProfile.tsx (Client Component)
'use client'
interface UserProfileProps {
name: string
email: string
joinedAt: string // string として受け取る
}
export function UserProfile({ name, email, joinedAt }: UserProfileProps) {
const joinedDate = new Date(joinedAt) // クライアント側でDate変換
return (
<div>
<h2>{name}</h2>
<p>{email}</p>
<p>登録日: {joinedDate.toLocaleDateString('ja-JP')}</p>
</div>
)
}
3. サードパーティライブラリの扱い
多くのReactライブラリ(UI コンポーネントライブラリ等)は、内部でReactフックを使用しているため、Client Componentとしてマークする必要があります。
しかし、ライブラリのコードを直接編集することはできないため、ラッパーコンポーネントを作成します:
// components/ClientChart.tsx
'use client'
import { Chart } from 'third-party-chart-library'
// ラッパーコンポーネントでClient Component化
export function ClientChart(props) {
return <Chart {...props} />
}
// app/analytics/page.tsx (Server Component)
import { ClientChart } from '@/components/ClientChart'
export default async function AnalyticsPage() {
const data = await fetchAnalyticsData()
return (
<div>
<h1>アナリティクス</h1>
{/* ラッパーを通してサードパーティライブラリを使用 */}
<ClientChart data={data} />
</div>
)
}
実際のパフォーマンス影響
Next.js 15のドキュメントによると、Server Componentsを適切に使用することで、クライアントに送信されるJavaScriptバンドルサイズを大幅に削減できます。
以下は、ECサイトの商品一覧ページでの比較例です:
すべてClient Componentsの場合:
- 初期JavaScriptバンドル: 250KB(gzip圧縮後)
- First Contentful Paint (FCP): 1.8秒
- Time to Interactive (TTI): 3.2秒
Server ComponentsとClient Componentsを適切に分離した場合:
- 初期JavaScriptバンドル: 85KB(gzip圧縮後)
- First Contentful Paint (FCP): 1.1秒
- Time to Interactive (TTI): 1.8秒
JavaScriptバンドルサイズを66%削減し、FCPは0.7秒、TTIは1.4秒改善されました。
flowchart LR
A["ページリクエスト"] --> B{"コンポーネント分類"}
B -->|静的コンテンツ| C["Server Components"]
B -->|インタラクティブ| D["Client Components"]
C --> E["サーバーレンダリング"]
E --> F["HTML送信 (小)"]
D --> G["JSバンドル生成"]
G --> H["JS送信 (大)"]
F --> I["高速初期表示"]
H --> J["ハイドレーション"]
I --> K["パフォーマンス最適化"]
J --> K
Server Componentsを活用することで、クライアントに送信されるJavaScriptを最小化し、パフォーマンスを向上させます
よくある間違いとその修正方法
間違い1: Server ComponentsでReactフックを使用
// ❌ エラー: Server ComponentsではuseStateは使用できない
export default function Counter() {
const [count, setCount] = useState(0) // Error!
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
修正:
// ✅ Client Componentとしてマーク
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
間違い2: Client ComponentsでServer-only コードを使用
// ❌ エラー: Client Componentsでデータベース直接アクセス
'use client'
import { db } from '@/lib/database'
export function UserList() {
const users = db.users.findMany() // Error! dbはクライアントで利用不可
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
修正:
// ✅ Server Componentでデータ取得
import { UserList } from './UserList'
import { db } from '@/lib/database'
export default async function UsersPage() {
const users = await db.users.findMany()
// データのみをClient Componentに渡す
return <UserList users={users} />
}
// Client Component
'use client'
export function UserList({ users }) {
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
間違い3: Client ComponentsをServer Componentsにインポート
Server ComponentsはClient Componentsをインポートできますが、その逆はできません。
// ❌ エラー: Client ComponentsからServer Componentsをインポート
'use client'
import ServerComponent from './ServerComponent' // Error!
export function ClientComponent() {
return <ServerComponent />
}
修正:
Server Componentsを親に配置し、Client Componentsを子として扱います:
// ✅ Server Componentを親に
import ClientComponent from './ClientComponent'
export default function ParentServerComponent() {
return (
<div>
<h1>サーバーでレンダリング</h1>
<ClientComponent />
</div>
)
}
まとめ
Next.js 15におけるServer ComponentsとClient Componentsの使い分けは、パフォーマンス最適化の核心です。以下のポイントを押さえることで、効率的なアプリケーションを構築できます:
- デフォルトはServer Components: すべてのコンポーネントはデフォルトでServer Componentsとして扱われ、クライアントに送信されるJavaScriptを削減します
- インタラクティブ性が必要な場合のみClient Components:
useState,useEffect, イベントリスナーなど、Reactフックやブラウザイベントが必要な場合にのみ'use client'を使用します - Client Componentsはツリーの「葉」に配置: コンポーネントツリーの末端にClient Componentsを配置し、親はServer Componentとして保ちます
- シリアライズ可能なデータのみ渡す: Server ComponentsからClient Componentsには、JSON形式でシリアライズ可能なデータのみを渡します
- サードパーティライブラリはラッパーで対応: Reactフックを使用するライブラリは、Client Componentのラッパーで包んで使用します
これらのベストプラクティスを実践することで、JavaScriptバンドルサイズを最小化し、初期表示速度とユーザー体験を大幅に改善できます。