レンダリング戦略の全体像
Next.jsは、1つのフレームワーク内で5つのレンダリング戦略を提供しています。
Pages Router時代はgetStaticProps/getServerSidePropsの選択が中心でしたが、
App Router + Cache Componentsの時代では、より細粒度でページ内の各部分に異なるレンダリング戦略を適用できるようになりました。
| 戦略 | レンダリングタイミング | TTFB | 適したコンテンツ |
|---|---|---|---|
| SSG | ビルド時 | 最速(CDN配信) | ブログ、ドキュメント、LP |
| ISR | ビルド時 + バックグラウンド再生成 | 高速(staleデータ即返却) | ECサイト、ニュース |
| SSR | リクエスト時 | 中程度(サーバー処理待ち) | 認証付きダッシュボード |
| Streaming | リクエスト時(段階的配信) | 高速(シェル即配信) | 複雑なデータ依存ページ |
| PPR | ビルド時(静的シェル)+ リクエスト時(動的部分) | 最速級(静的シェル即配信) | ECトップ、パーソナライズページ |
SSG — Static Site Generation
SSG(Static Site Generation)は、ビルド時にHTMLを生成する最もシンプルなレンダリング戦略です。 生成されたHTMLはCDNにキャッシュされ、リクエスト時にはサーバー処理なしで即座に配信されます。
App Routerでは、動的APIを使用せず、動的ルートパラメータをgenerateStaticParamsで事前に列挙することでSSGが有効になります。
デフォルトの挙動として、ビルド時にすべてのデータが確定するページは自動的にSSGとして処理されます。
// app/blog/[slug]/page.tsx — SSGの実装
// generateStaticParamsで全ページのパラメータを事前生成
export async function generateStaticParams() {
const posts = await fetch('https://cms.example.com/posts')
.then(r => r.json());
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}));
}
// ビルド時に各slugに対してこの関数が実行される
export default async function BlogPost(
{ params }: { params: Promise<{ slug: string }> }
) {
const { slug } = await params;
const post = await fetch(
'https://cms.example.com/posts/' + slug
).then(r => r.json());
return (
<article>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<p>{post.body}</p>
</article>
);
}
// dynamicParams = false にすると、generateStaticParamsに含まれないパスは404を返す
export const dynamicParams = false; SSR — Server-Side Rendering
SSR(Server-Side Rendering)は、リクエストのたびにサーバー側でHTMLを生成する戦略です。 ユーザーごとに異なるコンテンツを表示する必要がある場合や、リクエスト時の情報(Cookie、ヘッダー等)に依存する場合に使用します。
App Routerでは、コンポーネント内でcookies()、headers()、searchParamsなどの
動的APIを使用すると、自動的にSSRモードに切り替わります。明示的にdynamic = 'force-dynamic'を設定することも可能です。
// app/dashboard/page.tsx — SSRの実装(動的API使用で自動判定)
import { cookies } from 'next/headers';
export default async function Dashboard() {
// cookies()を使用 → 自動的にSSR(リクエスト時レンダリング)
const cookieStore = await cookies();
const token = cookieStore.get('auth-token')?.value;
const data = await fetch('https://api.example.com/dashboard', {
headers: { Authorization: 'Bearer ' + token },
}).then(r => r.json());
return (
<div>
<h1>{data.userName}のダッシュボード</h1>
<p>最終ログイン: {data.lastLogin}</p>
</div>
);
}
// 明示的にSSRを強制する方法
// export const dynamic = 'force-dynamic'; ISR — Incremental Static Regeneration
ISR(Incremental Static Regeneration)は、SSGとSSRのハイブリッドです。 初回はビルド時に静的生成したHTMLを返し、指定した時間が経過すると バックグラウンドで再生成を行い、次のリクエストから最新のHTMLを配信します。
App Routerでは、Route Segmentにrevalidateを設定するか、
前章で解説した'use cache' + cacheLife()で同等の挙動を実現できます。
// 方法1: Route Segment ConfigによるISR
export const revalidate = 3600; // 1時間ごとに再検証
// 方法2: 'use cache' + cacheLife()による同等の制御
import { cacheLife } from 'next/cache';
export default async function ProductPage(
{ params }: { params: Promise<{ id: string }> }
) {
'use cache';
cacheLife('hours'); // stale: 5分, revalidate: 1時間, expire: 1日
const { id } = await params;
const product = await fetch(
'https://api.example.com/products/' + id
).then(r => r.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}円</p>
</div>
);
} Streaming — SuspenseとloadingUIによる段階的配信
Streamingは、ページ全体の生成を待たずに、準備ができた部分から段階的にHTMLをクライアントに送信する戦略です。
ReactのSuspense境界とNext.jsのloading.tsxを組み合わせることで実現します。
従来のSSRでは、ページ内のすべてのデータフェッチが完了するまでユーザーは何も表示されませんでした。 Streamingでは、データ取得に時間がかかる部分をSuspenseで囲み、その間はfallback UIを表示します。 データが到着次第、該当部分がクライアント側で差し替えられます。
// app/dashboard/page.tsx — Streamingの実装
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
<h1>ダッシュボード</h1>
{/* ナビゲーション: 即座に表示 */}
<nav>...</nav>
{/* 売上データ: 取得に2秒かかる → Suspenseで段階的配信 */}
<Suspense fallback={<SalesSkeleton />}>
<SalesChart />
</Suspense>
{/* ユーザー一覧: 取得に3秒かかる → 別のSuspense境界 */}
<Suspense fallback={<UserListSkeleton />}>
<RecentUsers />
</Suspense>
</div>
);
}
// 各コンポーネントはServer Componentとして独立してデータを取得
async function SalesChart() {
const sales = await fetch('https://api.example.com/sales')
.then(r => r.json());
return <div>{/* チャート描画 */}</div>;
}
async function RecentUsers() {
const users = await fetch('https://api.example.com/users')
.then(r => r.json());
return <ul>{/* ユーザー一覧 */}</ul>;
} sequenceDiagram
participant Browser as ブラウザ
participant Server as サーバー
participant DB1 as 売上API(2秒)
participant DB2 as ユーザーAPI(3秒)
Browser->>Server: GET /dashboard
Server-->>Browser: HTMLシェル + ナビ + Skeleton UI(即座)
Note over Browser: ナビゲーションとSkeleton表示済み
Server->>DB1: 売上データ取得
Server->>DB2: ユーザーデータ取得
DB1-->>Server: 売上データ(2秒後)
Server-->>Browser: SalesChartのHTML(ストリーム送信)
Note over Browser: SalesSkeletonがSalesChartに置換
DB2-->>Server: ユーザーデータ(3秒後)
Server-->>Browser: RecentUsersのHTML(ストリーム送信)
Note over Browser: UserListSkeletonがRecentUsersに置換Partial Prerendering(PPR) — 静的シェル + 動的ストリーミング
Partial Prerendering(PPR)は、Next.js v15で実験的に導入された次世代のレンダリング戦略です。 1つのページ内で静的な部分と動的な部分を明確に分離し、 静的シェルをCDNから即座に配信しつつ、動的部分をストリーミングで後から埋めるという SSGとStreamingの融合を実現します。
// next.config.ts — PPRの有効化
const nextConfig = {
experimental: {
ppr: 'incremental', // ルートごとに段階的に有効化
},
};
export default nextConfig;
// app/product/[id]/page.tsx — PPRの実装
import { Suspense } from 'react';
// このルートでPPRを有効化
export const experimental_ppr = true;
export default function ProductPage(
{ params }: { params: Promise<{ id: string }> }
) {
return (
<div>
{/* 静的シェル: ビルド時に生成、CDNキャッシュ */}
<header>
<h1>商品詳細</h1>
<nav>...</nav>
</header>
{/* 動的部分: Suspense境界内がリクエスト時にストリーミング */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice params={params} />
</Suspense>
<Suspense fallback={<RecommendationSkeleton />}>
<PersonalizedRecommendations />
</Suspense>
{/* 静的部分: 商品説明、レビュー(ビルド時生成) */}
<section>
<h2>商品説明</h2>
<p>...</p>
</section>
</div>
);
} graph TB
subgraph ビルド時
A[ページ全体をレンダリング] --> B{Suspense境界を検出}
B --> C[静的シェルHTML生成]
B --> D[動的ホール(プレースホルダ)記録]
C --> E[CDNにデプロイ]
end
subgraph リクエスト時
F[ユーザーがアクセス] --> G[CDNから静的シェル即配信<br/>TTFB ≈ 40ms]
G --> H[ブラウザが静的HTMLを即座に描画]
G --> I[サーバーが動的部分を生成開始]
I --> J[動的HTMLをストリーミング送信]
J --> K[ブラウザがSkeleton→実コンテンツに置換]
end
style C fill:#10b981,stroke:#059669,color:#fff
style D fill:#f97316,stroke:#ea580c,color:#fff
style G fill:#3b82f6,stroke:#1d4ed8,color:#fff
style J fill:#8b5cf6,stroke:#6d28d9,color:#fffPPRの最大の利点はTTFBの劇的な改善です。 静的シェルがCDNから配信されるため、TTFB(Time To First Byte)は従来のSSRの350ms程度から40ms程度に短縮されます。 ユーザーはページのレイアウトとナビゲーションを即座に確認でき、 パーソナライズされたコンテンツやリアルタイムデータは数秒後に段階的に表示されます。
Cache Components時代のレンダリングモデル
'use cache'ディレクティブの導入により、従来のSSG/SSR/ISRという分類は
より柔軟なStatic / Cached / Dynamicの3分類に進化しつつあります。
| 分類 | 生成タイミング | キャッシュ | 旧来の対応概念 |
|---|---|---|---|
| Static | ビルド時に確定 | 永続(再デプロイまで) | SSG |
| Cached | ビルド時 or 初回リクエスト時 | cacheLife()で制御 | ISR / unstable_cache |
| Dynamic | リクエスト時 | なし | SSR |
重要なのは、これら3つの分類がページ単位ではなくコンポーネント単位で混在できる点です。 PPRとCache Componentsの組み合わせにより、1つのページ内でStaticなヘッダー、Cachedな商品情報、 Dynamicなユーザー固有のレコメンデーションを共存させることが自然にできます。
// 1つのページ内で3つのレンダリングモデルが共存
import { Suspense } from 'react';
import { cacheLife, cacheTag } from 'next/cache';
export const experimental_ppr = true;
// Static: ビルド時に確定するヘッダー
function SiteHeader() {
return <header><h1>My Store</h1></header>;
}
// Cached: 1時間ごとに再検証される商品データ
async function ProductInfo({ id }: { id: string }) {
'use cache';
cacheLife('hours');
cacheTag('product-' + id);
const product = await fetch(
'https://api.example.com/products/' + id
).then(r => r.json());
return <div><h2>{product.name}</h2></div>;
}
// Dynamic: リクエスト時にユーザーごとに生成
async function Recommendations() {
const { cookies } = await import('next/headers');
const cookieStore = await cookies();
const userId = cookieStore.get('user-id')?.value;
const recs = await fetch(
'https://api.example.com/recommendations?user=' + userId
).then(r => r.json());
return <ul>{/* レコメンド表示 */}</ul>;
}
// ページ: Static + Cached + Dynamic の混合
export default function ProductPage(
{ params }: { params: Promise<{ id: string }> }
) {
return (
<div>
<SiteHeader />
<Suspense fallback={<div>読み込み中...</div>}>
<ProductInfo id={params.then(p => p.id)} />
</Suspense>
<Suspense fallback={<div>おすすめ読み込み中...</div>}>
<Recommendations />
</Suspense>
</div>
);
} Server Componentsによるストリーミングフロー
Server Componentsのストリーミングは、内部的にHTML、RSC Payload、Hydrationの3段階で処理されます。 このフローを理解することで、パフォーマンスのボトルネックを特定しやすくなります。
sequenceDiagram
participant B as ブラウザ
participant S as Next.jsサーバー
participant R as Reactランタイム
B->>S: HTTPリクエスト
Note over S: Phase 1: 初期HTML生成
S->>R: Server Componentツリーのレンダリング開始
R-->>S: 静的部分のHTML + Suspense fallback
S-->>B: Transfer-Encoding: chunked<br/>初期HTML(シェル + Skeleton)
Note over B: ブラウザが即座にHTMLを描画<br/>FCP達成
Note over S: Phase 2: RSC Payloadストリーミング
R-->>S: 非同期データ到着 → RSC Payload生成
S-->>B: script タグ経由でRSC Payload送信
B->>B: RSC PayloadからDOMを更新<br/>Skeleton → 実コンテンツに置換
Note over S: Phase 3: Client Component Hydration
S-->>B: Client ComponentのJSバンドル
B->>B: Selective Hydration<br/>インタラクション可能に(TTI達成)TTFB改善パターン
レンダリング戦略の選択は、Core Web Vitalsの中でも特にTTFB(Time To First Byte)に大きな影響を与えます。 以下は、ECサイトの商品詳細ページにおける実測ベースの改善事例です。
| 戦略 | TTFB | LCP | 特徴 |
|---|---|---|---|
| SSR(従来型) | ~350ms | ~1,200ms | すべてのデータ取得を待ってからHTML送信 |
| SSR + Streaming | ~120ms | ~800ms | シェルを即送信、データは段階的に配信 |
| ISR | ~50ms | ~400ms | CDNキャッシュから即配信(staleデータの可能性) |
| PPR | ~40ms | ~350ms | 静的シェルをCDN配信 + 動的部分をストリーミング |
レンダリング戦略の使い分けマトリクス
最適なレンダリング戦略は、コンテンツの特性によって異なります。 以下のマトリクスを判断基準として活用してください。
| 要件 | 推奨戦略 | 理由 |
|---|---|---|
| コンテンツが完全に静的(ブログ、ドキュメント) | SSG | ビルド時に確定、CDN配信で最速 |
| 定期的に更新されるコンテンツ(EC商品、ニュース) | ISR / Cached | stale-while-revalidateで鮮度と速度を両立 |
| ユーザー認証が必要(ダッシュボード) | SSR + Streaming | 動的APIの使用が必須、Streamingで体感速度を改善 |
| 静的部分と動的部分が混在(ECトップ) | PPR | 静的シェルの即配信 + 動的部分のストリーミング |
| リアルタイム性が重要(チャット、株価) | SSR + クライアントサイド更新 | 初期HTMLはSSR、以降はWebSocket等でリアルタイム更新 |
| SEOが重要 + パーソナライズあり | PPR or ISR + Streaming | 静的部分でSEO確保、動的部分でパーソナライズ |
まとめ — レンダリング戦略の進化
Next.jsのレンダリング戦略は、SSG/SSRの二択から始まり、ISRによるハイブリッド化、 Streamingによる段階的配信、そしてPPRによる静的と動的の融合へと進化してきました。 Cache Componentsの導入により、ページ単位ではなくコンポーネント単位で レンダリング戦略を選択できる時代に入っています。
最も重要なのは、「すべてのページに1つの戦略を適用する」という発想から脱却し、 ページ内の各パーツの特性に応じて最適な戦略を組み合わせるという設計思想を持つことです。 PPRとSuspenseの組み合わせにより、この思想が自然なコードとして表現できるようになりました。
次章では、これらのレンダリング戦略を活かしたパフォーマンス最適化の実践テクニック、 Core Web VitalsのLCP/INP/CLS改善手法、React Compilerによる自動メモ化について解説します。
理解度チェック
Partial Prerendering(PPR)の仕組みとして正しい説明はどれですか?
キーボード: 1〜4 で選択、Enter で回答