メインコンテンツへスキップ
#Web制作 約12分で読めます

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 ComponentsClient 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バンドルサイズを最小化し、初期表示速度とユーザー体験を大幅に改善できます。

参考リンク

#Next.js #React #Server Components #パフォーマンス最適化 #App Router
シェア