Skip to content

CSR・SSR詳細

📚 Reactの基礎について: 基本的なReactの概念(useState、useEffect、コンポーネントなど)については、Reactガイドを参照してください。このドキュメントでは、Next.jsでのレンダリング戦略に焦点を当てます。

Next.jsは、複数のレンダリング戦略を提供しており、それぞれに異なる特徴とユースケースがあります。このガイドでは、各戦略の詳細な仕組み、実装方法、そして実務での使い分けについて詳しく解説します。

Next.jsで利用可能なレンダリング戦略は以下の通りです:

戦略略称レンダリングタイミング特徴
Client-Side RenderingCSRブラウザ(クライアント)インタラクティブ、SEOに弱い
Server-Side RenderingSSRサーバー(リクエスト時)SEOに強い、サーバー負荷あり
Static Site GenerationSSGビルド時最速、SEOに強い、動的コンテンツ不可
Incremental Static RegenerationISRビルド時 + 再生成SSG + 動的更新

レンダリングフローの視覚的理解

Section titled “レンダリングフローの視覚的理解”

ブラウザとサーバーの間でどのようにデータとHTMLが動くのか、その違いを理解することが設計の第一歩です。

レストランの比喩で理解するレンダリング戦略

Section titled “レストランの比喩で理解するレンダリング戦略”

各レンダリング戦略を、レストランの「調理」に例えると理解しやすくなります。

比喩:

  • サーバー(キッチン)は材料(JavaScript/データ)を提供
  • ブラウザ(客席)が自分で調理(レンダリング)を行う
  • 材料が届くまで客(ユーザー)は待たされる

実際のフロー:

  1. サーバーが空のHTMLとJavaScriptを送信
  2. ブラウザがJavaScriptをダウンロード・実行
  3. JavaScriptがAPIからデータを取得
  4. データを使ってDOMを構築
  5. ユーザーにコンテンツが表示される

特徴:

  • ✅ インタラクティブ(調理方法を変更可能)
  • ❌ 初回表示が遅い(材料が届くまで待つ必要がある)

シーケンス図:

CSRのレンダリングフロー

フローの詳細説明:

  1. ユーザーがページリクエストを送信: ブラウザがサーバーに対してページの取得を要求します。この時点では、ユーザーには何も表示されていません。

  2. サーバーが空のHTMLとJavaScriptを送信: サーバーは、最小限のHTML構造(通常は空の<div>要素)と、Reactアプリケーション全体を含むJavaScriptバンドルを送信します。このHTMLには実際のコンテンツは含まれていません。

  3. ブラウザがJavaScriptをダウンロード・実行: ブラウザは送信されたJavaScriptファイルをダウンロードし、実行を開始します。この処理には時間がかかることがあり、特にモバイル環境やネットワークが遅い場合には顕著です。

  4. JavaScriptがAPIからデータを取得: 実行されたJavaScriptコードが、外部APIや内部APIエンドポイントに対してデータ取得リクエストを送信します。この時点でも、まだユーザーにはコンテンツが表示されていません。

  5. APIがデータを返却: APIサーバーがデータを返却します。このデータには、商品情報、ユーザー情報、ブログ記事など、ページに表示する必要がある情報が含まれています。

  6. JavaScriptがDOMを構築: 取得したデータを使用して、JavaScriptがDOM(Document Object Model)を構築します。Reactの場合は、仮想DOMを構築し、実際のDOMに反映させます。

  7. コンテンツが表示される: 最終的に、ユーザーのブラウザにコンテンツが表示されます。この時点で初めて、ユーザーはページの内容を確認できます。

注意点: このプロセス全体で、ユーザーは最初の数秒間(場合によってはそれ以上)「白い画面」を見ることになります。これがCSRの主な欠点です。

SSR: サーバーが「調理」を済ませてから提供

Section titled “SSR: サーバーが「調理」を済ませてから提供”

比喩:

  • サーバー(キッチン)が調理(レンダリング)を完了
  • 調理済みの料理(HTML)をすぐに提供
  • 客(ユーザー)はすぐに食べられる
  • 注文(リクエスト)のたびに調理が必要

実際のフロー:

  1. ユーザーがページにアクセス
  2. サーバーがデータを取得
  3. サーバーがHTMLを生成
  4. 完全なHTMLがブラウザに送信
  5. ユーザーにコンテンツが即座に表示される

特徴:

  • ✅ SEOに強い(検索エンジンがコンテンツを認識可能)
  • ✅ 初回表示が速い
  • ❌ サーバー負荷が高い(リクエストごとに処理が必要)

シーケンス図:

SSRのレンダリングフロー

フローの詳細説明:

  1. ユーザーがページリクエストを送信: ブラウザがサーバーに対してページの取得を要求します。この時点では、まだコンテンツは生成されていません。

  2. サーバーがAPIからデータを取得: サーバー側で、必要なデータを外部APIやデータベースから取得します。この処理はサーバー側で行われるため、ブラウザの処理能力に依存しません。

  3. APIがデータを返却: APIサーバーがデータを返却します。このデータは、ページに表示する必要があるすべての情報を含んでいます。

  4. サーバーがHTMLを生成(レンダリング): サーバー側で、取得したデータを使用して完全なHTMLを生成します。Reactコンポーネントがサーバー上で実行され、HTML文字列に変換されます。この時点で、コンテンツが完全に含まれたHTMLが完成します。

  5. サーバーが完全なHTMLを送信: 生成された完全なHTMLがブラウザに送信されます。このHTMLには、すべてのコンテンツが含まれているため、検索エンジンのクローラーも内容を認識できます。

  6. ブラウザがコンテンツを即座に表示: ブラウザは受け取ったHTMLを即座に表示します。ユーザーは、JavaScriptのダウンロードや実行を待つことなく、すぐにコンテンツを確認できます。

  7. ハイドレーション(オプション): その後、JavaScriptがダウンロードされ、サーバーで生成されたHTMLにインタラクティブな機能が追加されます(ハイドレーション)。これにより、ボタンクリックなどのインタラクションが可能になります。

注意点: この方式では、リクエストごとにサーバーで処理が行われるため、サーバーの負荷が高くなります。また、データ取得に時間がかかる場合、ユーザーがページを表示するまでに時間がかかることがあります。

SSG: 事前に「作り置き」しておく

Section titled “SSG: 事前に「作り置き」しておく”

比喩:

  • 事前に作り置き(ビルド時にHTMLを生成)
  • 注文と同時にすぐに提供可能
  • メニュー(内容)の変更には再調理(ビルド)が必要

実際のフロー:

  1. ビルド時にデータを取得
  2. ビルド時にHTMLを生成
  3. 静的ファイルとして保存
  4. CDNから配信
  5. ユーザーに最速でコンテンツが表示される

特徴:

  • ✅ 最速のパフォーマンス
  • ✅ SEOに強い
  • ✅ サーバー負荷が最小
  • ❌ 動的なコンテンツには不向き

シーケンス図:

SSGのレンダリングフロー

フローの詳細説明:

ビルド時(事前準備):

  1. ビルドプロセスが開始: アプリケーションをデプロイする前に、Next.jsのビルドプロセスが実行されます。この時点で、すべてのページのHTMLが事前に生成されます。

  2. APIからデータを取得: ビルド時に、各ページに必要なデータをAPIやデータベースから取得します。例えば、ブログ記事の一覧、商品情報、会社概要などが取得されます。

  3. データが返却される: APIサーバーがデータを返却します。このデータは、ビルド時点での最新の情報です。

  4. HTMLを生成(レンダリング): 取得したデータを使用して、各ページの完全なHTMLを生成します。Reactコンポーネントが実行され、HTML文字列に変換されます。

  5. 静的ファイルとして保存: 生成されたHTMLが静的ファイルとして保存されます。これらのファイルは、CDN(Content Delivery Network)に配置され、世界中のユーザーに高速に配信されます。

ユーザーアクセス時(配信):

  1. ユーザーがページリクエストを送信: ブラウザがCDNに対してページの取得を要求します。この時点で、HTMLは既に生成済みです。

  2. CDNが事前生成されたHTMLを送信: CDNは、事前に生成されたHTMLファイルを即座に送信します。CDNは通常、ユーザーに最も近いサーバーから配信するため、非常に高速です。

  3. ブラウザがコンテンツを即座に表示: ブラウザは受け取ったHTMLを即座に表示します。サーバー側での処理やデータ取得の待ち時間がないため、最速のパフォーマンスを実現できます。

注意点: この方式では、ビルド時点でのデータしか表示できません。データが更新された場合、新しいビルドとデプロイが必要になります。そのため、頻繁に更新されるコンテンツには不向きです。

比喩:

  • 事前に作り置き(SSGと同様)
  • 一定期間ごとに自動で再調理(再生成)
  • 新しい料理(コンテンツ)が提供可能

実際のフロー:

  1. ビルド時にHTMLを生成(SSGと同様)
  2. ユーザーに最速でコンテンツを提供
  3. 指定した期間経過後、バックグラウンドで再生成
  4. 次のリクエストから新しいコンテンツを提供

特徴:

  • ✅ SSGの速度 + 動的コンテンツの更新
  • ✅ SEOに強い
  • ✅ ビルド時間を短縮可能

シーケンス図:

ISRのレンダリングフロー

フローの詳細説明:

ビルド時(初期生成):

  1. ビルドプロセスが開始: SSGと同様に、ビルド時に初回のHTMLを生成します。これにより、最初のユーザーアクセス時には最速のパフォーマンスを提供できます。

  2. APIからデータを取得: ビルド時に、必要なデータをAPIから取得します。

  3. データが返却される: APIサーバーがデータを返却します。

  4. 初回HTMLを生成: 取得したデータを使用して、初回のHTMLを生成し、CDNに保存します。

ユーザーアクセス時(初回):

  1. ユーザーがページリクエストを送信: 最初のユーザーがページにアクセスします。

  2. CDNが事前生成されたHTMLを送信: CDNは、事前に生成されたHTMLを即座に送信します。この時点では、SSGと同様に最速のパフォーマンスを実現します。

再生成期間経過後:

  1. 再生成期間が経過: 設定した期間(例:1時間、1日)が経過すると、次のユーザーアクセス時に再生成がトリガーされます。

  2. ユーザーがページリクエストを送信: 再生成期間が経過した後のユーザーアクセスが発生します。

  3. サーバーが最新データを取得: サーバーが、最新のデータをAPIから取得します。この時点で、データベースやAPIの最新情報が反映されます。

  4. APIが最新データを返却: APIサーバーが最新のデータを返却します。

  5. サーバーがHTMLを再生成: 取得した最新データを使用して、HTMLを再生成します。この処理は、ユーザーのリクエストに応じてバックグラウンドで行われます。

  6. 更新されたHTMLをCDNに保存: 再生成されたHTMLがCDNに保存され、次のユーザーアクセス時から使用されます。

  7. 最新コンテンツをユーザーに表示: 再生成された最新のコンテンツがユーザーに表示されます。初回のユーザーは少し待つ必要がありますが、その後のユーザーは最新のコンテンツを最速で閲覧できます。

注意点: ISRは、SSGの速度とSSRの動的更新の両方の利点を組み合わせた方式です。ただし、再生成期間の設定が重要で、短すぎるとサーバー負荷が高くなり、長すぎると古いデータが表示される可能性があります。

CSRは、レンダリングをブラウザに任せる方式です。サーバーは最低限のHTMLとJavaScriptを送信し、データ取得やDOM構築はすべてクライアント(ブラウザ)で行われます。

// CSRのレンダリングフロー
// 1. サーバーが空のHTMLとJavaScriptを送信
// 2. ブラウザがJavaScriptをダウンロード・実行
// 3. JavaScriptがAPIからデータを取得
// 4. データを使ってDOMを構築

実装例:

📚 useStateとuseEffectの詳細: useStateとuseEffectの詳細な使い方については、ReactガイドのuseStateとuseEffectを参照してください。

app/products/page.tsx
'use client';
// 注意: useStateとuseEffectの詳細については、Reactガイドを参照してください
import { useState, useEffect } from 'react';
export default function ProductsPage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// クライアントサイドでデータを取得
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
});
}, []);
if (loading) {
return <div>読み込み中...</div>;
}
return (
<div>
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.price}</p>
</div>
))}
</div>
);
}

利点:

  • 高速なページ遷移: 初回ロード後は、必要なデータだけをAPIで取得し、DOMを部分的に更新するため、ページ間の移動が非常に滑らか
  • インタラクティブ: ユーザーの操作に応じて動的に変化するUI(例:フォーム入力、グラフ)の実装に適している
  • サーバー負荷が少ない: サーバーは静的なファイルを配信するだけなので、負荷が少ない

欠点:

  • 初回表示までの遅延: JavaScriptとデータがすべてロードされるまで、ユーザーは何も表示されない「白い画面」を見ることになる
  • SEOへの影響: 検索エンジンのクローラーが初期に完全なコンテンツを取得できないため、SEOのパフォーマンスに影響する可能性がある
  • パフォーマンス指標への影響: FCP (First Contentful Paint) や LCP (Largest Contentful Paint) が遅くなる
// ✅ CSRが適しているケース
// 1. 管理画面やダッシュボード(SEO不要)
// 注意: useStateの詳細については、Reactガイドを参照してください
'use client';
export default function AdminDashboard() {
const [data, setData] = useState(null);
// ...
}
// 2. リアルタイムなデータ表示(WebSocket使用)
// 注意: useStateとuseEffectの詳細については、Reactガイドを参照してください
'use client';
export default function LiveChart() {
const [prices, setPrices] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/prices');
ws.onmessage = (event) => {
setPrices(JSON.parse(event.data));
};
}, []);
// ...
}
// 3. インタラクティブなフォーム
'use client';
export default function ContactForm() {
const [formData, setFormData] = useState({});
// リアルタイムバリデーションなど
// ...
}

CSR、SSR、SSGの違いを理解するためには、それぞれのDOM構造がどのように異なるかを理解することが重要です。

CSR、SSR、SSGのDOM構造の違い

各レンダリング戦略におけるDOM構造:

CSR(Client-Side Rendering)のDOM構造

Section titled “CSR(Client-Side Rendering)のDOM構造”

サーバーから送信されるHTML:

<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<!-- 空のdivのみ -->
<script src="/app.js"></script>
</body>
</html>

ブラウザでの最終的なDOM構造:

  • JavaScriptが実行され、DOMが動的に構築される
  • 初期状態では空の<div id="root">のみ
  • JavaScriptの実行後、コンテンツが挿入される

SSR(Server-Side Rendering)のDOM構造

Section titled “SSR(Server-Side Rendering)のDOM構造”

サーバーから送信されるHTML:

<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">
<h1>商品一覧</h1>
<div>商品1: 1000円</div>
<div>商品2: 2000円</div>
<!-- 完全なコンテンツが含まれている -->
</div>
<script src="/app.js"></script>
</body>
</html>

ブラウザでのDOM構造:

  • サーバーで生成された完全なHTMLがそのまま表示される
  • ユーザーは即座にコンテンツを確認できる
  • その後、JavaScriptがダウンロードされ、ハイドレーションが実行される

SSG(Static Site Generation)のDOM構造

Section titled “SSG(Static Site Generation)のDOM構造”

CDNから配信されるHTML:

<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<div id="root">
<h1>ブログ記事</h1>
<article>記事の本文...</article>
<!-- ビルド時に生成された完全なコンテンツ -->
</div>
<script src="/app.js"></script>
</body>
</html>

ブラウザでのDOM構造:

  • ビルド時に生成された完全なHTMLがCDNから配信される
  • サーバー側の処理が不要で、最速で表示される
  • SSRと同様に、完全なコンテンツが含まれている

レンダリング戦略の移行に関するベストプラクティスとバッドプラクティス

Section titled “レンダリング戦略の移行に関するベストプラクティスとバッドプラクティス”

既存のアプリケーションを異なるレンダリング戦略に移行する際は、それぞれの戦略の特性と制約を理解することが重要です。

1. CSRからSSRへの直接的な移行はできない

Section titled “1. CSRからSSRへの直接的な移行はできない”

問題点: CSRで書かれたコードをそのままSSRに移行することはできません。CSRでは、ブラウザのAPI(windowdocumentlocalStorageなど)を直接使用できますが、SSRではサーバー側で実行されるため、これらのAPIは使用できません。

// ❌ バッドプラクティス: CSRからSSRへの直接的な移行
// app/products/page.tsx(元々CSR)
'use client';
import { useState, useEffect } from 'react';
export default function ProductsPage() {
const [products, setProducts] = useState([]);
useEffect(() => {
// 問題: windowオブジェクトを使用(サーバーでは存在しない)
const apiUrl = window.location.origin + '/api/products';
// 問題: localStorageを使用(サーバーでは存在しない)
const token = localStorage.getItem('token');
fetch(apiUrl, {
headers: { Authorization: `Bearer ${token}` }
})
.then(res => res.json())
.then(data => setProducts(data));
}, []);
return <div>{/* ... */}</div>;
}
// このコードを単純にSSRに移行すると、サーバー側でエラーが発生する
// export default async function ProductsPage() { ... } // ❌ エラー

なぜ問題なのか:

  • windowdocumentlocalStorageなどのブラウザAPIは、サーバー側では存在しない
  • useEffectはクライアントサイドでのみ実行されるため、SSRでは使用できない
  • コンポーネントの実行環境(サーバー vs ブラウザ)が異なるため、コードを書き直す必要がある

2. すべてのページを一度に移行しようとする

Section titled “2. すべてのページを一度に移行しようとする”

問題点: 大規模なアプリケーションを一度に移行しようとすると、多くの問題が発生し、リスクが高くなります。

// ❌ バッドプラクティス: すべてを一度に移行
// 問題: 数千ページあるアプリケーションを一度に移行しようとする
// 問題: テストが困難で、問題の特定が難しい
// 問題: ロールバックが困難

なぜ問題なのか:

  • 移行による影響範囲が広すぎる
  • 問題の特定と修正が困難
  • 本番環境での影響が大きい

3. ハイドレーション・ミスマッチを無視する

Section titled “3. ハイドレーション・ミスマッチを無視する”

問題点: サーバーで生成されたHTMLとクライアントで生成されるHTMLが異なると、ハイドレーションエラーが発生します。

// ❌ バッドプラクティス: ハイドレーション・ミスマッチを無視
export default function Page() {
// 問題: サーバーとクライアントで異なる値になる
const random = Math.random();
const timestamp = new Date().toLocaleString();
return (
<div>
<p>ランダム値: {random}</p>
<p>時刻: {timestamp}</p>
</div>
);
}

推奨アプローチ: Next.js App Routerでは、ページごとに異なるレンダリング戦略を使用できます。まず、影響が少ないページから段階的に移行します。

// ✅ ベストプラクティス: 段階的な移行
// ステップ1: 新規ページはSSRで作成
// app/blog/[slug]/page.tsx(新規、SSRで作成)
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = await getPost(slug);
return <Article post={post} />;
}
// ステップ2: 既存のCSRページは維持(必要に応じて)
// app/dashboard/page.tsx(既存、CSRのまま)
'use client';
export default function Dashboard() {
// CSRのまま維持
// ...
}
// ステップ3: 徐々に重要なページをSSRに移行
// app/products/page.tsx(移行対象、段階的に移行)
export default async function ProductsPage() {
// SSRに移行
const products = await getProducts();
return <ProductList products={products} />;
}

メリット:

  • リスクを最小限に抑えられる
  • 問題が発生した場合、影響範囲を限定できる
  • テストが容易になる

2. コンポーネントの分離(サーバーコンポーネントとクライアントコンポーネント)

Section titled “2. コンポーネントの分離(サーバーコンポーネントとクライアントコンポーネント)”

推奨アプローチ: ページ全体をSSRに移行するのではなく、サーバーコンポーネントとクライアントコンポーネントを適切に分離します。

// ✅ ベストプラクティス: コンポーネントの適切な分離
// app/products/page.tsx(サーバーコンポーネント)
export default async function ProductsPage() {
// サーバーサイドでデータを取得
const products = await getProducts();
return (
<div>
<h1>商品一覧</h1>
{/* サーバーコンポーネントでデータを渡す */}
<ProductList products={products} />
</div>
);
}
// components/ProductList.tsx(クライアントコンポーネント)
'use client';
import { useState } from 'react';
export default function ProductList({ products }: { products: Product[] }) {
const [filteredProducts, setFilteredProducts] = useState(products);
const [category, setCategory] = useState('all');
// クライアントサイドでのインタラクション
const handleFilter = (newCategory: string) => {
setCategory(newCategory);
setFilteredProducts(
newCategory === 'all'
? products
: products.filter(p => p.category === newCategory)
);
};
return (
<div>
<button onClick={() => handleFilter('electronics')}>
電子機器
</button>
{filteredProducts.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}

メリット:

  • データ取得はサーバーサイドで効率的に実行
  • インタラクティブな機能はクライアントサイドで実現
  • SEOとユーザー体験の両方を最適化

3. ハイドレーション・ミスマッチの回避

Section titled “3. ハイドレーション・ミスマッチの回避”

推奨アプローチ: 環境に依存する値は、サーバーサイドで確定するか、クライアントサイドでのみ使用するように設計します。

app/page.tsx
// ✅ ベストプラクティス: ハイドレーション・ミスマッチの回避
// 方法1: サーバーサイドで確定した値を渡す
export default async function HomePage() {
const serverTime = new Date(); // サーバーサイドで確定
return <TimeDisplay serverTime={serverTime} />;
}
// components/TimeDisplay.tsx
export default function TimeDisplay({ serverTime }: { serverTime: Date }) {
return <div>サーバー時刻: {serverTime.toLocaleString()}</div>;
}
// 方法2: クライアントサイドでのみ処理
// components/ClientTimeDisplay.tsx
'use client';
import { useState, useEffect } from 'react';
export default function ClientTimeDisplay() {
const [time, setTime] = useState<Date | null>(null);
useEffect(() => {
// クライアントサイドでのみ実行
setTime(new Date());
}, []);
if (!time) return <div>読み込み中...</div>;
return <div>クライアント時刻: {time.toLocaleString()}</div>;
}

メリット:

  • ハイドレーションエラーを防止
  • サーバーとクライアントで一貫した動作を保証
  • パフォーマンスの向上

4. データフェッチング方法の適切な選択

Section titled “4. データフェッチング方法の適切な選択”

推奨アプローチ: SSRに移行する際は、データフェッチングの方法を適切に選択します。

// ✅ ベストプラクティス: データフェッチングの適切な選択
// SSR: リクエストごとに最新のデータを取得
export default async function NewsPage() {
const news = await fetch('https://api.example.com/news', {
cache: 'no-store', // キャッシュしない
}).then(res => res.json());
return <NewsList news={news} />;
}
// SSG: ビルド時にデータを取得
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: false }, // キャッシュ(SSG)
}).then(res => res.json());
return <BlogList posts={posts} />;
}
// ISR: 定期的に再生成
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const product = await fetch(`https://api.example.com/products/${id}`, {
next: { revalidate: 3600 }, // 1時間ごとに再生成
}).then(res => res.json());
return <ProductDetail product={product} />;
}

既存のCSRアプリケーションをSSRに移行する際のチェックリスト:

  • ブラウザAPIの使用を確認: windowdocumentlocalStorageなどのブラウザAPIを使用していないか確認
  • コンポーネントの実行環境を確認: サーバーサイドで実行されることを前提にコードを書き直す
  • データフェッチングの方法を変更: useEffectでのデータ取得から、サーバーサイドでのデータ取得に変更
  • 状態管理の見直し: クライアントサイドの状態管理を適切に分離
  • ハイドレーション・ミスマッチの回避: 環境に依存する値の処理を適切に設計
  • 段階的な移行: 一度にすべてを移行せず、ページごとに段階的に移行
  • テストの実施: 移行後のページが正常に動作することを確認

SSRは、レンダリングをサーバーで行う方式です。ユーザーからのリクエストに対して、サーバーがデータを取得し、完成したHTMLを生成してブラウザに送信します。

// SSRのレンダリングフロー
// 1. ユーザーがリクエストを送る
// 2. サーバーがデータを取得し、HTMLを生成
// 3. サーバーが完成したHTMLをブラウザに送信
// 4. ブラウザがHTMLをすぐに表示
// 5. JavaScriptがロードされ、ハイドレーションが実行される

実装例(App Router):

app/products/[id]/page.tsx
// デフォルトでサーバーコンポーネント(SSR)
async function getProduct(id: string) {
// サーバーサイドでデータを取得
const res = await fetch(`https://api.example.com/products/${id}`, {
cache: 'no-store', // SSRの場合はキャッシュしない
});
return res.json();
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}</p>
<p>{product.description}</p>
</div>
);
}

実装例(Pages Router):

pages/products/[id].tsx
import { GetServerSideProps } from 'next';
type ProductPageProps = {
product: {
id: string;
name: string;
price: number;
description: string;
};
};
export default function ProductPage({ product }: ProductPageProps) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}</p>
<p>{product.description}</p>
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params!;
const product = await fetch(`https://api.example.com/products/${id}`)
.then(res => res.json());
return {
props: {
product,
},
};
};

利点:

  • 高速な初回表示: ユーザーはすぐにコンテンツを見ることができ、ユーザー体験が向上する
  • 優れたSEO: 検索エンジンのクローラーは完成したHTMLを受け取るため、コンテンツを正確にインデックスできる
  • 最新データの表示: リクエストごとにデータを取得するため、常に最新のデータを表示できる

欠点:

  • サーバー負荷: リクエストごとにサーバーがHTMLを生成するため、アクセス数が増えるとサーバーの負荷が高まる
  • ページ遷移の遅延: ページを移動するたびにサーバーが新しいHTMLを生成するため、CSRに比べると遷移が遅く感じることがある
  • TTFB (Time to First Byte) の増加: サーバーでHTMLを生成する時間がかかるため、TTFBが増加する
// ✅ SSRが適しているケース
// 1. SEOが重要なページ(ブログ、ECサイト)
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <Article post={post} />;
}
// 2. ユーザー固有のデータを表示するページ
export default async function Dashboard() {
const user = await getCurrentUser();
const data = await getUserData(user.id);
return <DashboardContent data={data} />;
}
// 3. 頻繁に更新されるコンテンツ
export default async function NewsPage() {
const news = await fetch('https://api.example.com/news', {
cache: 'no-store', // 常に最新のデータを取得
}).then(res => res.json());
return <NewsList news={news} />;
}

SSGは、ビルド時にページを事前にレンダリングし、CDNにデプロイすることで、ユーザーからのリクエストに対して静的なHTMLファイルを直接提供するアプローチです。

// SSGのレンダリングフロー
// 1. ビルド時にすべてのページをレンダリング
// 2. 静的なHTMLファイルを生成
// 3. CDNにデプロイ
// 4. ユーザーがリクエストを送る
// 5. CDNから静的なHTMLを直接配信

実装例(App Router):

app/blog/[slug]/page.tsx
// ビルド時に生成するパスを定義
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({
slug: post.slug,
}));
}
// 静的ページとして生成
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}

実装例(Pages Router):

pages/blog/[slug].tsx
import { GetStaticProps, GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getPosts();
return {
paths: posts.map(post => ({
params: { slug: post.slug },
})),
fallback: false, // 404を返す
};
};
export const getStaticProps: GetStaticProps = async (context) => {
const { slug } = context.params!;
const post = await getPost(slug);
return {
props: {
post,
},
};
};

利点:

  • 極めて高速な表示: ユーザーはサーバーにリクエストを送る必要がなく、CDNから直接HTMLを受け取るため、非常に高速
  • サーバー負荷なし: ページはビルド時に一度だけ生成されるため、トラフィックの増加がサーバーに影響を与えない
  • 優れたSEO: SSRと同様に、クローラーは完全にレンダリングされたHTMLを受け取る
  • コスト削減: サーバーリソースが不要なため、コストを削減できる

欠点:

  • 動的コンテンツの制限: ビルド時に生成されるため、リクエストごとに異なるコンテンツを表示できない
  • 再ビルドが必要: コンテンツが更新された場合、再ビルドと再デプロイが必要
  • ビルド時間の増加: ページ数が増えると、ビルド時間が長くなる
// ✅ SSGが適しているケース
// 1. ブログやドキュメントサイト(更新頻度が低い)
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}
// 2. ランディングページ(静的コンテンツ)
export default function LandingPage() {
return (
<div>
<h1>ようこそ</h1>
<p>私たちについて</p>
</div>
);
}
// 3. ポートフォリオサイト
export default function Portfolio() {
const projects = getProjects(); // ビルド時に取得
return <ProjectList projects={projects} />;
}

ISRは、SSGとSSRの中間的なアプローチです。ビルド時にページを生成しますが、指定した時間間隔で再生成することで、動的なコンテンツにも対応できます。

// ISRのレンダリングフロー
// 1. ビルド時にページを生成(SSG)
// 2. ユーザーがリクエストを送る
// 3. CDNから静的なHTMLを配信
// 4. 再生成の時間が経過した場合、バックグラウンドで再生成
// 5. 次のリクエストから新しいHTMLを配信

実装例(App Router):

app/products/[id]/page.tsx
export const revalidate = 3600; // 1時間ごとに再生成
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { revalidate: 3600 }, // 1時間キャッシュ
}).then(res => res.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}</p>
</div>
);
}

実装例(Pages Router):

pages/products/[id].tsx
export const getStaticProps: GetStaticProps = async (context) => {
const { id } = context.params!;
const product = await getProduct(id);
return {
props: {
product,
},
revalidate: 3600, // 1時間ごとに再生成
};
};

利点:

  • SSGの利点を維持: 高速な表示とサーバー負荷の軽減
  • 動的コンテンツに対応: 指定した時間間隔で再生成することで、動的なコンテンツにも対応
  • 段階的な再生成: すべてのページを再生成する必要がなく、必要なページだけを再生成できる

欠点:

  • 再生成の遅延: 再生成の時間が経過するまで、古いコンテンツが表示される可能性がある
  • 複雑さ: SSGやSSRに比べて、設定が複雑になる
// ✅ ISRが適しているケース
// 1. ECサイトの商品ページ(更新頻度が中程度)
export const revalidate = 3600; // 1時間ごとに再生成
// 2. ブログのトップページ(最新記事を表示)
export const revalidate = 1800; // 30分ごとに再生成
// 3. ニュースサイト(頻繁に更新されるが、リアルタイム性は不要)
export const revalidate = 600; // 10分ごとに再生成

Next.jsのApp Routerは、これらのレンダリング方法を組み合わせて利用するハイブリッドレンダリングを標準としています。

サーバーコンポーネントとクライアントコンポーネント

Section titled “サーバーコンポーネントとクライアントコンポーネント”
app/products/[id]/page.tsx
// デフォルトでサーバーコンポーネント(SSR)
import AddToCartButton from '@/components/AddToCartButton';
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
cache: 'no-store', // SSR
});
return res.json();
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return (
<div>
{/* サーバーコンポーネントでレンダリング(SSR) */}
<h1>{product.name}</h1>
<p>{product.price}</p>
<p>{product.description}</p>
{/* クライアントコンポーネント(CSR) */}
<AddToCartButton productId={product.id} />
</div>
);
}
components/AddToCartButton.tsx
'use client'; // クライアントコンポーネントとして宣言
import { useState } from 'react';
export default function AddToCartButton({ productId }: { productId: string }) {
const [isAdding, setIsAdding] = useState(false);
const handleAddToCart = async () => {
setIsAdding(true);
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId }),
});
setIsAdding(false);
};
return (
<button
onClick={handleAddToCart}
disabled={isAdding}
>
{isAdding ? '追加中...' : 'カートに追加'}
</button>
);
}

ハイブリッドレンダリングの利点

Section titled “ハイブリッドレンダリングの利点”
  • パフォーマンス: 初期ロードが速く、SEOに有利
  • 開発体験: サーバーとクライアントのロジックが分離され、コードの見通しが良くなる
  • 柔軟性: ページごとに最適なレンダリング戦略を選択できる
// 判断基準1: コンテンツの更新頻度
// 静的コンテンツ → SSG
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}
// 動的コンテンツ → SSR
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
cache: 'no-store' // SSR
});
return <div>{data.title}</div>;
}
// 中程度の更新頻度 → ISR
export const revalidate = 3600; // 1時間ごとに再生成
// 判断基準2: ユーザー固有かどうか
// ユーザー固有 → SSR
export default async function Dashboard() {
const user = await getCurrentUser();
const data = await getUserData(user.id);
return <Dashboard data={data} />;
}
// 全ユーザー共通 → SSG / ISR
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <Article post={post} />;
}
// 判断基準3: SEOの重要性
// SEO重要 → SSR / SSG
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
return <ProductDetails product={product} />;
}
// SEO不要 → CSR
'use client';
export default function AdminPanel() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <AdminContent data={data} />;
}
要件推奨戦略理由
SEOが重要SSR / SSG検索エンジンがコンテンツを認識できる
初回表示速度が重要SSG / ISRCDNから直接配信されるため最速
ユーザー固有のデータSSRリクエストごとにデータを取得
リアルタイム性が必要CSRクライアントサイドで動的に更新
更新頻度が低いSSGビルド時に生成すれば十分
更新頻度が中程度ISR定期的に再生成することで対応
更新頻度が高いSSR常に最新のデータを表示

レンダリング戦略によるパフォーマンス指標の違い

Section titled “レンダリング戦略によるパフォーマンス指標の違い”
// CSRの問題: 初期表示までの時間
// 1. HTMLのダウンロード: 50ms
// 2. JavaScriptのダウンロード: 200ms
// 3. JavaScriptの実行: 100ms
// 4. API呼び出し: 300ms
// 5. DOMの構築: 50ms
// 合計: 700ms(この間、ユーザーは白い画面を見る)
// SSRの利点: 即座にコンテンツが表示される
// 1. HTMLのダウンロード: 50ms(コンテンツが含まれている)
// 2. JavaScriptのダウンロード: 200ms(並行実行)
// 3. ハイドレーション: 100ms
// 合計: 350ms(ユーザーはすぐにコンテンツを見られる)
// SSGの利点: 最速の表示
// 1. HTMLのダウンロード: 10ms(CDNから直接配信)
// 合計: 10ms(最も高速)
// 実際のパフォーマンス指標への影響
// CSR: FCP (First Contentful Paint) = 700ms
// SSR: FCP = 50ms(約14倍の改善)
// SSG: FCP = 10ms(約70倍の改善)
// CSRの問題: 検索エンジンがコンテンツを認識できない
// 1. クローラーがHTMLを取得 → 空のdivのみ
// 2. JavaScriptを実行(時間がかかる、失敗する可能性)
// 3. コンテンツが表示される(遅延、または失敗)
// SSRの利点: 検索エンジンが即座にコンテンツを認識
// 1. クローラーがHTMLを取得 → 完全なコンテンツが含まれている
// 2. インデックス可能
// SSGの利点: SSRと同様にSEOに強い
// 1. クローラーがHTMLを取得 → 完全なコンテンツが含まれている
// 2. インデックス可能
// 実際のSEOスコアへの影響
// CSR: SEOスコア = 60/100(コンテンツが認識されない)
// SSR: SEOスコア = 95/100(コンテンツが完全に認識される)
// SSG: SEOスコア = 95/100(コンテンツが完全に認識される)

戦略的・設計的判断フロー(意思決定マトリクス)

Section titled “戦略的・設計的判断フロー(意思決定マトリクス)”

どのページにどの戦略を適用すべきか、以下のフローチャートに沿って設計します。

ステップ1: コンテンツの性質を特定する

Section titled “ステップ1: コンテンツの性質を特定する”

質問1: 「そのページはログインなしで見れるか?」

Section titled “質問1: 「そのページはログインなしで見れるか?」”

NO(マイページ、設定等)→ SSR または CSR(SEO不要のため)

// ログイン必須ページの例
// app/dashboard/page.tsx(SSR)
export default async function DashboardPage() {
// ユーザー固有のデータを取得
const user = await getCurrentUser();
return <div>ダッシュボード: {user.name}</div>;
}

YES → 質問2に進む

質問2: 「検索エンジンにインデックスさせたいか?」

Section titled “質問2: 「検索エンジンにインデックスさせたいか?」”

YES(ブログ、商品詳細、LP)→ SSR または SSG/ISR

NO → CSR(SEO不要のため)

質問3: 「データは頻繁に変わるか?」

Section titled “質問3: 「データは頻繁に変わるか?」”

NO(会社概要、利用規約)→ SSG

// 静的なコンテンツの例
// app/about/page.tsx(SSG)
export default async function AboutPage() {
// ビルド時にデータを取得(キャッシュされる)
const data = await fetch('https://api.example.com/about', {
next: { revalidate: false } // 再生成しない
});
return <div>{data.content}</div>;
}

YES → 質問4に進む

質問4: 「リアルタイム性が重要か?」

Section titled “質問4: 「リアルタイム性が重要か?」”

YES(在庫状況、リアルタイムチャット)→ SSR

// リアルタイム性が重要なページの例
// app/inventory/[productId]/page.tsx(SSR)
export default async function InventoryPage({ params }: { params: Promise<{ productId: string }> }) {
const { productId } = await params;
// リクエストごとに最新の在庫を取得
const inventory = await fetch(`https://api.example.com/inventory/${productId}`, {
cache: 'no-store' // キャッシュしない(常に最新)
});
return <div>在庫: {inventory.stock}</div>;
}

NO(数分〜数時間の猶予がある場合)→ ISR

// 定期的に更新が必要なページの例
// app/news/[slug]/page.tsx(ISR)
export default async function NewsPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
// 1時間ごとに再生成
const news = await fetch(`https://api.example.com/news/${slug}`, {
next: { revalidate: 3600 } // 1時間(3600秒)ごとに再生成
});
return <div>{news.title}</div>;
}

ステップ2: Client vs Server コンポーネントの境界線

Section titled “ステップ2: Client vs Server コンポーネントの境界線”

App Routerでは「ページ全体」ではなく「コンポーネント単位」で戦略を分けます。

コンポーネントの種類主な役割・設計戦略使うべきシーン
Server (Default)データの取得・加工(重い処理)API連携、DBアクセス、秘匿情報の利用
Client (‘use client’)対話(インタラクション)onClick, useState, useEffect, ブラウザAPI
// ✅ 良い例: サーバーコンポーネントとクライアントコンポーネントの適切な分離
// app/products/page.tsx(サーバーコンポーネント)
export default async function ProductsPage() {
// サーバーサイドでデータを取得
const products = await fetch('https://api.example.com/products');
const data = await products.json();
return (
<div>
<h1>商品一覧</h1>
{/* クライアントコンポーネントを使用 */}
<ProductList products={data.products} />
</div>
);
}
// components/ProductList.tsx(クライアントコンポーネント)
'use client';
import { useState } from 'react';
export default function ProductList({ products }: { products: Product[] }) {
const [selectedCategory, setSelectedCategory] = useState('all');
// クライアントサイドでのインタラクション
const filteredProducts = products.filter(p =>
selectedCategory === 'all' || p.category === selectedCategory
);
return (
<div>
<button onClick={() => setSelectedCategory('electronics')}>
電子機器
</button>
{filteredProducts.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
);
}

【実戦】高度なレンダリング設計戦略

Section titled “【実戦】高度なレンダリング設計戦略”

シニアエンジニアが考慮すべき、より深い設計判断です。

A. 「外側はサクサク、内側は最新」戦略 (SSR + CSR)

Section titled “A. 「外側はサクサク、内側は最新」戦略 (SSR + CSR)”

ページ全体をSSRにするのではなく、骨組み(Layoutや主要コンテンツ)はサーバーで生成し、**ユーザーごとの動的な部分(お気に入りボタン、コメント一覧)だけをクライアントサイドで取得(SWRやReact Query)**します。

メリット:

  • SEOを確保しつつ、サーバーのレスポンス(TTFB)を高速化できる
  • ユーザー固有のデータを効率的に取得できる

実装例:

// app/blog/[slug]/page.tsx(サーバーコンポーネント)
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
// サーバーサイドで基本的なコンテンツを取得
const post = await fetch(`https://api.example.com/posts/${slug}`, {
next: { revalidate: 3600 }
}).then(res => res.json());
return (
<div>
{/* サーバーサイドでレンダリング(SEO対応、高速) */}
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* ユーザー固有のデータはクライアントサイドで取得 */}
<FavoriteButton postId={post.id} />
<CommentList postId={post.id} />
</div>
);
}
// components/FavoriteButton.tsx(クライアントコンポーネント)
'use client';
import useSWR from 'swr';
export default function FavoriteButton({ postId }: { postId: string }) {
// クライアントサイドでユーザー固有のデータを取得
const { data: favorite } = useSWR(`/api/favorites/${postId}`, fetcher);
return (
<button>
{favorite ? 'お気に入り解除' : 'お気に入りに追加'}
</button>
);
}

B. 「静的生成 + 部分更新」戦略 (SSG + ISR)

Section titled “B. 「静的生成 + 部分更新」戦略 (SSG + ISR)”

ECサイトなどで、数万ページある商品詳細をすべてビルド時に作るのは現実的ではありません。

戦略: 人気商品の上位100件だけを SSG で事前生成し、残りはユーザーがアクセスした時に ISR (fallback: ‘blocking’) でオンデマンド生成する。

メリット:

  • ビルド時間を短縮しつつ、全ページで高速な表示を実現
  • 人気商品は最速で表示、長尾商品もオンデマンドで高速化

実装例:

app/products/[productId]/page.tsx
export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
const { productId } = await params;
// ISR: 1時間ごとに再生成、初回アクセス時にオンデマンド生成
const product = await fetch(`https://api.example.com/products/${productId}`, {
next: { revalidate: 3600 } // 1時間ごとに再生成
}).then(res => res.json());
return (
<div>
<h1>{product.name}</h1>
<p>価格: {product.price}</p>
</div>
);
}
// 人気商品の上位100件はビルド時に生成(generateStaticParams)
export async function generateStaticParams() {
const popularProducts = await fetch('https://api.example.com/products/popular?limit=100')
.then(res => res.json());
return popularProducts.map((product: Product) => ({
productId: product.id,
}));
}

C. パフォーマンスの落とし穴:ハイドレーション・ミスマッチ

Section titled “C. パフォーマンスの落とし穴:ハイドレーション・ミスマッチ”

SSR/SSGにおいて、サーバーが生成したHTMLとクライアントが最初に生成しようとした内容が異なると(例:new Date() の利用)、エラーが発生します。

設計ルール: 時間やランダムな値など、環境に依存する値は useEffect 内で処理するか、サーバーサイドで確定させた値を渡すように設計を統一します。

// ❌ 悪い例: ハイドレーションミスマッチが発生
export default function Page() {
const now = new Date(); // サーバーとクライアントで異なる値になる
return <div>現在時刻: {now.toLocaleString()}</div>;
}
// ✅ 良い例1: サーバーサイドで確定した値を渡す
export default async function Page() {
const now = new Date(); // サーバーサイドで確定
return <ServerDateDisplay date={now} />;
}
// ✅ 良い例2: クライアントサイドで処理
'use client';
import { useState, useEffect } from 'react';
export default function ClientDateDisplay() {
const [date, setDate] = useState<Date | null>(null);
useEffect(() => {
// クライアントサイドでのみ実行
setDate(new Date());
}, []);
if (!date) return <div>読み込み中...</div>;
return <div>現在時刻: {date.toLocaleString()}</div>;
}

D. Next.js 15のキャッシュのデフォルト挙動の変化

Section titled “D. Next.js 15のキャッシュのデフォルト挙動の変化”

重要: Next.js 15では、fetchのデフォルト挙動が変更されました。以前はfetchは自動的にキャッシュされていましたが、Next.js 15ではcache: 'no-store'がデフォルトになっています。

Next.js 14以前:

// 自動的にキャッシュされる(デフォルト)
const data = await fetch('https://api.example.com/data');

Next.js 15:

// デフォルトでキャッシュされない(cache: 'no-store'がデフォルト)
const data = await fetch('https://api.example.com/data', {
cache: 'no-store' // 明示的に指定(デフォルトだが、明示的に書くことを推奨)
});
// キャッシュを有効にする場合
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // ISR: 1時間ごとに再生成
});

影響:

  • SSRがデフォルトの挙動になったため、意図せずSSRになっている可能性がある
  • SSG/ISRを使用する場合は、明示的にnext: { revalidate }を指定する必要がある
  • パフォーマンスに影響を与える可能性があるため、キャッシュ戦略を意識的に設計する必要がある

「全てのページをSSGにする」のが正解ではありません。以下の3つのバランスを取ることが重要です。

最速でLCP(最大視覚コンテンツの表示)を完了させる。

サーバー負荷とビルド時間のバランスを取る。

'use client'を最小限に抑え、ロジックをサーバーサイドに寄せることで、クライアントサイドのJS量を削減する。

この「境界線の設計」こそが、Next.jsエンジニアの腕の見せ所です。


  • 新規プロジェクト: App Routerを使用し、デフォルトでサーバーコンポーネント(SSR)を活用
  • SEOが重要: SSRまたはSSGを使用
  • 初回表示速度が重要: SSGまたはISRを使用
  • ユーザー固有のデータ: SSRを使用
  • リアルタイム性が必要: SSRを使用(CSRも検討)
  • ハイブリッドアプローチ: サーバーコンポーネントとクライアントコンポーネントを組み合わせる
  • Next.js 15対応: キャッシュ戦略を意識的に設計する(cache: 'no-store'がデフォルト)

これらのベストプラクティスを守ることで、パフォーマンスが高く、SEOに強く、ユーザー体験の優れたNext.jsアプリケーションを構築できます。