Next.js のレンダリング戦略とは「ユーザーの行動・状況に合わせて、どこで・いつ・何を描画するかを最適化する仕組み」である。SEO強化は副産物に過ぎず、本質はユーザー体験とサーバー負荷のトレードオフを柔軟に制御することにある。
"use client"で明示)の責務分担が明確化され、APIフェッチの量が大幅に削減されたMPAからSPAへ、そしてハイブリッドへ——各アプローチはトレードオフの産物である。
MPA時代(〜2010年代前半)
サーバー → HTML生成 → ブラウザ(フルリロード)
SPA時代(2013〜)
サーバー → JSバンドル → ブラウザ(クライアントでDOM構築)
問題:初期表示が遅い / 白い画面 / スピナー / ネットワーク往復が2回
ハイブリッド時代(現在)
サーバー側でHTMLを生成しつつ、クライアント側でインタラクティブに動かす
→ SSR / SSG / ISR / Server Components
| MPA | SPA | ハイブリッド | |
|---|---|---|---|
| 初期表示 | ✅ 速い | ❌ 遅い | ✅ 速い |
| インタラクティブ性 | ❌ 低い | ✅ 高い | ✅ 高い |
| SEO | ✅ 強い | ❌ 弱い | ✅ 強い |
| サーバー負荷 | 高い | 低い | 中〜高 |
💡 ポイント: 「SPAで十分では?」という問いへの答えは「用途次第」。管理画面・ログイン後のダッシュボードはSPAで十分。公開ページ・ECサイト・SEOが必要なコンテンツはハイブリッドが現実解。ハイブリッドのメリットは「SEO・初期表示速度・インタラクティブ性を同時に得られる」点にある。
React は「UIを作るライブラリ」であり、アプリケーション全体を作るフレームワークではない。React 公式ドキュメント自体がフレームワーク使用を推奨している。
React 本体が提供するのはコンポーネントツリーの描画とステート管理のみで、ルーティング・データフェッチ・SSR・バンドル最適化は自前で揃える必要がある。
React 単体でSPAを作る場合に自前で用意するもの:
React
├── ルーティング → React Router / TanStack Router
├── データフェッチ → TanStack Query / SWR
├── SSR対応 → フレームワーク
├── バンドル・最適化 → Vite / Webpack
└── 画像最適化・環境変数管理 → 自前
| Next.js | TanStack Start | Remix | |
|---|---|---|---|
| 主な用途 | フルスタック・あらゆる規模 | SPAよりのフルスタック | フォーム・MPA寄り |
| レンダリング | CSR/SSR/SSG/ISR | CSR/SSR | SSR中心 |
| ルーティング | ファイルベース | TanStack Router | ファイルベース |
| Vercel親和性 | ◎(Vercel製) | △ | △ |
| 採用実績 | ◎ 最多 | △ 新興 | 〇 一定数あり |
| 向いているケース | ECサイト・企業サイト・SaaS | データ可視化・管理画面 | コンテンツ・フォーム中心 |
💡 ポイント: TanStack Query(データフェッチライブラリ)と TanStack Start(フレームワーク)は別物。Next.js + TanStack Query の組み合わせも非常に一般的。
Next.js は「React の足りない部分を全部埋めるフレームワーク」として生まれ、現在はサーバー・クライアントの境界を再定義する段階に進化している。
Next.js の進化:
v1〜v12(Pages Router時代)
pages/ ディレクトリ = 1ファイル1ページコンポーネント
getServerSideProps → SSR
getStaticProps → SSG / ISR
API Routes → サーバー処理
v13〜(App Router時代)
app/ ディレクトリ = フォルダ構造でルーティング
Server Components → デフォルトでサーバーで実行
Client Components → "use client" で明示
Server Actions → "use server" でサーバー処理を関数単位で定義
→ getServerSideProps などの特殊関数が不要に
従来のSPA + APIサーバー構成では、サーバーはJSONを返すだけだった。App Router 時代はHTMLを返すNext.jsサーバーが加わり、2層構造になる。
従来(SPA + APIサーバー):
ブラウザ → CDN(空HTML + JS)→ APIサーバー(JSON)→ ブラウザ(DOM構築)
App Router時代:
ブラウザ → Next.jsサーバー(完成HTML)→ ブラウザ(即表示)
操作後 → Server Action → Next.jsサーバー(DB更新 + 差分HTML)→ ブラウザ
APIサーバー(Django)が残るケースは以下:
| Pages Router | App Router | |
|---|---|---|
| ルーティング | pages/ のファイル名 | app/ のフォルダ構造 |
| ページ定義 | ファイル = ページ | page.tsx が存在するフォルダ = ページ |
| SSR/SSG指定 | getServerSideProps / getStaticProps | fetch のオプション / dynamic 設定 |
| サーバー処理 | ページ単位 | コンポーネント単位(Server Components) |
| デフォルト | CSR | Server Component(SSR寄り) |
| CSRの指定 | 何も書かない | "use client" を明示 |
新規プロジェクトは App Router 一択。既存Pages Routerは急いで移行する必要はなく、両方並行サポートされている。
本質は「ユーザーの行動・状況に合わせて、どこで・いつ・何を描画するかを最適化すること」であり、SEOの強化はその副産物に過ぎない。
「ビルド」には2種類ある。
① フロントエンドのビルド(全戦略共通)
JSX / TypeScript → JS バンドル への変換・最適化
→ CSR も SSR も SSG も全部やる(next build で実行)
② SSG/ISR 固有の「HTMLの事前生成」
ビルド時に実際にページをレンダリングして
.html ファイルとして出力しておく
| JSビルド | HTML事前生成 | |
|---|---|---|
| CSR | ✅ | ❌(空のHTMLのみ) |
| SSR | ✅ | ❌(リクエストのたびに生成) |
| SSG | ✅ | ✅(全ページ分) |
| ISR | ✅ | ✅(初回 + 再生成) |
HTMLを事前生成できる?(全ユーザー共通・初回表示)
└── Yes → SSG / ISR
└── No
├── ユーザー操作・状態管理のみ → CSR
└── サーバーデータが必要 → SSR / Server Action
3層の違いで整理すると:
CSR:
ネットワーク往復①:空HTML + JSダウンロード
ネットワーク往復②:APIフェッチ
体験:白い画面 → スピナー → データ表示
SSR:
ネットワーク往復①:完成HTMLダウンロード(サーバー処理時間含む)
体験:白い画面(HTML返却待ち)→ 完成HTML即表示 → スピナーなし
SSRもHTMLを待つ間は白い画面になる。体感差はスピナーが出るか出ないかと、ネットワーク往復が1回か2回かにある。
| CSR | SSR | SSG | ISR | |
|---|---|---|---|---|
| HTML生成場所 | ブラウザ | サーバー(リクエスト時) | サーバー(ビルド時) | サーバー(ビルド時+再生成) |
| 初期表示速度 | ❌ 遅い | ✅ 速い | ✅ 最速 | ✅ 最速 |
| SEO | ❌ 弱い | ✅ 強い | ✅ 強い | ✅ 強い |
| 最新データ | ✅ 常に最新 | ✅ 常に最新 | ❌ ビルド時点 | 🔺 設定次第 |
| パーソナライズ | ✅ 得意 | ✅ 得意 | ❌ 不可 | ❌ 不可 |
| サーバー負荷 | なし | 高 | なし | 低 |
| ホスティング | 静的のみ | Node.js必須 | 静的のみ | Node.js必須 |
CSRは「サーバーは静的ファイルを返すだけ・すべてのレンダリングをブラウザが担う」最もシンプルなモデル。App RouterではuseState / useEffectなどブラウザAPIが必要な末端コンポーネントのみが担当する。
① next build → 空のHTML + JSバンドルを生成
② S3 / CDN に配置(Node.jsサーバー不要)
③ ブラウザ → 空のHTML受け取り → JS実行 → DOM構築
App RouterにおけるCSR(Client Component)の役割:
useState / useEffect を使うインタラクティブUIonClick / onChange などのイベントハンドラー初期表示データの取得はServer Componentに任せるため、axiosの使用頻度は大幅に減る。axiosが残るのは「ユーザー操作後の追加フェッチ」のみ。
| 項目 | 評価 |
|---|---|
| 初期表示速度 | ❌ 遅い |
| SEO | ❌ 弱い |
| サーバー負荷 | ✅ なし |
| ホスティングコスト | ✅ 最安 |
| パーソナライズ | ✅ 得意 |
💡 ポイント: 管理画面・社内ツールなどSEO不要・ユーザー限定の場合はCSRが最もシンプル。Next.jsを使う必要すらなく、Vite + Reactで十分なケースも多い。
SSRは「リクエストのたびにサーバーがDBにアクセスしHTMLを生成して返す」モデル。DjangoのテンプレートレンダリングにHydrationを加えた概念に近い。
【ビルド時】
next build → JSバンドル生成のみ(DBアクセスなし)
【リクエスト時・毎回】
① ブラウザ → サーバーにリクエスト
② サーバー → DBアクセス・データ取得
③ サーバー → データを埋め込んだHTML生成
④ サーバー → HTMLをブラウザに返す
⑤ ブラウザ → HTML即表示
⑥ Hydration → インタラクティブに動く
ユーザー操作後のデータ変更はServer Actionで処理し、revalidatePathでServer Componentを再実行してHTMLの差分を返す。従来のJSON往復が不要になる。
// app/actions/order.ts
"use server"
export async function submitOrder(data: OrderData) {
await db.orders.create(data) // DB更新
revalidatePath('/orders') // Server Componentを再実行
}
重いDBクエリがあってもSuspenseで部分的にHTMLをストリーミング配信できる。
export default function Page() {
return (
<>
<Header /> {/* 即表示 */}
<Suspense fallback={<Spinner />}>
<SlowDataComponent /> {/* データ完了後に表示 */}
</Suspense>
<Footer /> {/* 即表示 */}
</>
)
}
| 項目 | 評価 |
|---|---|
| 初期表示速度 | ✅ 速い |
| SEO | ✅ 強い |
| 最新データ | ✅ 常に最新 |
| パーソナライズ | ✅ 得意 |
| サーバー負荷 | ❌ 高い(毎回DB+HTML生成) |
| キャッシュ効率 | ❌ 低い |
💡 ポイント: DjangoビューとSSRの違いはHydrationにある。リクエスト時にサーバーでデータを取得してHTMLを返す点は同一。Djangoテンプレートは静的なHTMLで終わるが、SSRはその後クライアント側でReactがインタラクティブに動く。
SSGは「ビルド時にDBアクセスしてHTMLを事前生成・CDNに配置する」モデル。リクエスト時にサーバー処理が一切不要なため最速・最低コストだが、ビルド後はデータが古くなる。
【ビルド時】
① DBアクセス → データ取得
② データを埋め込んだHTMLを生成・出力
③ CDN / S3 に配置
【リクエスト時】
④ ブラウザ → CDNにリクエスト
⑤ CDN → 生成済みHTMLをそのまま返す(サーバー処理なし)
⑥ Hydration → インタラクティブに
// App Router の SSG実装
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await db.getAllPosts() // ビルド時に全slugを取得
return posts.map(p => ({ slug: p.slug }))
}
export default async function BlogPost({ params }) {
const post = await db.getPost(params.slug) // ビルド時に実行
return <Article post={post} />
}
| 項目 | 評価 |
|---|---|
| 初期表示速度 | ✅ 最速(CDNから即返却) |
| SEO | ✅ 強い |
| 最新データ | ❌ ビルド時点で固定 |
| サーバー負荷 | ✅ ゼロ |
| ビルド時間 | ❌ ページ数に比例して増加 |
💡 ポイント: データが変わるたびに再ビルドが必要。CMSと組み合わせてWebhookで自動再ビルドするのが一般的。ページ数が数千・数万になるとビルド時間が問題になり、ISRが現実解になる。
ISRは「SSGの最速・低負荷を維持しながら、ビルド後もデータを更新できる」SSGの限界を解消した戦略。stale-while-revalidateモデルを採用している。
【ビルド時】SSGと同じ
DBアクセス → HTML生成 → CDNに配置
【リクエスト時・revalidate: 60の場合】
アクセス①(0秒) → 生成済みHTMLを返す
アクセス②(30秒後) → 生成済みHTMLを返す(まだ有効)
アクセス③(61秒後) → 古いHTMLを返す + バックグラウンドで再生成
アクセス④(62秒後) → 新しいHTMLを返す
重要:再生成のトリガーは「時間経過 + リクエスト」。誰もアクセスしなければ再生成されない。再生成中のユーザーは古いHTMLを受け取る(待たされない)。
時間ベースではなくデータ更新イベントをトリガーに即座に再生成する。CMSのWebhookと組み合わせるのが実用的。
// app/api/revalidate/route.ts
export async function POST(request: Request) {
const { path } = await request.json()
revalidatePath(path) // 該当ページを即再生成
return Response.json({ revalidated: true })
}
// ISRの実装(revalidateを追加するだけでSSG→ISRになる)
// app/products/page.tsx
export const revalidate = 60 // 60秒ごとに再生成
export default async function ProductsPage() {
const products = await db.getProducts()
return <ProductList products={products} />
}
| 項目 | 評価 |
|---|---|
| 初期表示速度 | ✅ 最速(CDNから返却) |
| SEO | ✅ 強い |
| 最新データ | 🔺 設定次第(数秒〜数時間の遅延あり) |
| サーバー負荷 | ✅ 低い(バックグラウンドのみ) |
💡 ポイント: 「再生成中に古いデータを返すのは問題では?」→ ECサイトの商品一覧など「多少古くても致命的でない」データには許容範囲。在庫数・価格など「古いと問題になる」データはSSRで対応するか、ISR + クライアントフェッチのハイブリッドにする。
「データの性質・更新頻度・ユーザー操作の有無」の3軸で戦略を選ぶ。1プロジェクト内でページごとに混在させるのが正しい設計。
HTMLを事前生成できる?(初回表示・全ユーザー共通)
└── Yes
├── 更新が必要?
│ ├── No → SSG
│ └── Yes
│ ├── 時間/イベントトリガー → ISR
│ └── リアルタイム → SSR
└── No(ユーザー操作・状態管理が必要)
├── サーバーデータが必要?
│ ├── Yes → Server Action(差分HTML取得)
│ └── No → CSR(useState / useEffect のみ)
| ページ | 戦略 | 理由 |
|---|---|---|
| トップページ | ISR | 数時間ごと更新 |
| 商品一覧 | ISR | 価格・在庫更新あり |
| 商品詳細 | ISR + CSR | 骨格はISR・在庫数はクライアントフェッチ |
| カート | CSR | ユーザー固有・SEO不要 |
| 注文確認 | SSR | 最新データ必須・ユーザー固有 |
| マイページ | SSR | ユーザー固有 |
| 会社概要・LP | SSG | 更新ほぼなし |
| ブログ記事 | SSG | 更新ほぼなし |
| 管理画面 | CSR | SEO不要・静的ホスティング可 |
💡 ポイント: Next.jsは1ページ内でISR + クライアントフェッチの混在が可能。「骨格はSSGで高速表示・リアルタイムデータだけクライアントフェッチ」というハイブリッドが実務の現実解になることが多い。
App Routerは「デフォルトがServer Component・"use client"で境界を引く・fetchオプションでSSR/SSG/ISRを制御する」の3原則で実装する。
app/
layout.tsx ← 全体レイアウト(Server Component)
page.tsx ← / トップページ
blog/
page.tsx ← /blog
[slug]/
page.tsx ← /blog/123(動的ルーティング)
api/
revalidate/
route.ts ← on-demand ISR用エンドポイント
actions/
cart.ts ← Server Actions("use server")
components/
ProductList.tsx ← Server Component(デフォルト)
AddToCartButton.tsx ← Client Component("use client"あり)
Page(Server Component)← 親・ほぼすべてのページ
├── Header(Server Component)
├── ProductList(Server Component)← DBアクセス
│ └── ProductCard(Server Component)
│ └── AddToCartButton(Client Component)← 末端のみ
└── Footer(Server Component)
importの方向:
✅ Server Component → Client Component をimport(OK)
❌ Client Component → Server Component をimport(エラー)
"use client" は境界線:
→ つけたコンポーネント以下はすべてClient Componentになる
→ 末端コンポーネントのみに限定する
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await db.getAllPosts() // ビルド時に全slugを取得
return posts.map(p => ({ slug: p.slug }))
}
export default async function BlogPost({ params }) {
const post = await db.getPost(params.slug) // ビルド時に実行
return <Article post={post} />
}
// app/products/page.tsx
export const revalidate = 60 // これだけでSSG→ISRになる
export default async function ProductsPage() {
const products = await db.getProducts()
return <ProductList products={products} />
}
// on-demand ISR(CMSのWebhookから呼ぶ)
// app/api/revalidate/route.ts
export async function POST(request: Request) {
const { path } = await request.json()
revalidatePath(path)
return Response.json({ revalidated: true })
}
// app/orders/page.tsx
export const dynamic = 'force-dynamic' // キャッシュなし・毎回サーバーでレンダリング
export default async function OrdersPage() {
const orders = await db.getOrders() // リクエストのたびに実行
return <OrderList orders={orders} />
}
// Server Action
// app/actions/cart.ts
"use server"
export async function addToCart(productId: string) {
const session = await getSession()
if (!session) throw new Error('Unauthorized') // 認証チェック必須
await db.cart.add(productId)
revalidatePath('/cart') // 忘れると画面が更新されない
}
// Server Component(親)
// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
const product = await db.getProduct(params.id)
return (
<>
<h1>{product.name}</h1>
<p>{product.price}円</p>
<AddToCartButton productId={params.id} /> {/* Client Component */}
</>
)
}
// Client Component(子・末端のみ)
// components/AddToCartButton.tsx
"use client"
import { addToCart } from '@/app/actions/cart'
export default function AddToCartButton({ productId }) {
const [added, setAdded] = useState(false)
return (
<button onClick={async () => {
await addToCart(productId) // 内部的にはHTTPリクエスト(開発者が意識不要)
setAdded(true)
}}>
{added ? '追加済み' : 'カートに追加'}
</button>
)
}
Server Actionのトリガーはボタンクリック・フォームsubmit・任意のイベント。内部的にはHTTPリクエスト(POST)だが、開発者は関数を呼ぶだけでよい。Django の urls.py + axios に相当する処理が不要になる。
// app/page.tsx
export default function Page() {
return (
<>
<Header /> {/* 即表示 */}
<Suspense fallback={<Spinner />}>
<SlowDataComponent /> {/* データ完了後に表示 */}
</Suspense>
<Footer /> {/* 即表示 */}
</>
)
}
// components/SlowDataComponent.tsx(Server Component)
export default async function SlowDataComponent() {
const data = await db.getSlowData() // 重いDBクエリ
return <DataView data={data} />
}
Next.jsはVercel以外でも動くが、自己ホストは運用コストと機能制限のトレードオフを理解した上で選択する。
メリット:
✅ Next.jsの全機能がそのまま動く(ISR・画像最適化・Edge Functions)
✅ デプロイがgit pushだけで完結
✅ CDNが自動で設定される
✅ プレビュー環境が自動生成される
✅ on-demand ISRのキャッシュ管理が自動
デメリット:
❌ 社内規定でAWS/GCP/Azure以外禁止の場合使えない
❌ データをVercelインフラに乗せられない(金融・医療・官公庁)
❌ トラフィックが大きいと割高
❌ boto3・SageMakerなどAWSエコシステムと密結合できない
❌ ベンダーロックインリスク
AWS ECS / EC2 / Cloud Runでの自己ホスト:
✅ 動く:
SSR・SSG・ISR(Node.jsサーバーがあれば動く)
Server Actions
画像最適化(sharpを自前インストール)
Route Handler
❌ 制限あり:
Edge Functions → Vercel固有・自己ホストでは動かない
ISRのキャッシュ管理 → 自前で設定が必要
CDN → CloudFrontなど別途設定が必要
Next.js on Vercel:
個人・スタートアップ・中規模SaaS
→ デプロイ・運用コストを最小化したい場合
Next.js on AWS ECS:
企業・規制業種・大規模サービス
→ AWS規制がある・ロックインを避けたい場合
Next.js + Django on AWS ECS:
→ boto3・ML処理が必要
→ モバイルアプリ向けAPIも必要
→ 既存Django資産がある
パターン別の現実解:
A(新規・Webのみ):
Next.js on Vercel → Supabase
→ 最もシンプル・運用コスト最小
B(既存資産活用):
Next.js on ECS → Django on ECS → RDS
→ 既存Django APIをそのまま活用
C(ハイブリッド):
Next.js on Vercel → Supabase(通常CRUD)
→ Django on ECS(ML・boto3のみ)
App Routerは概念の誤解・キャッシュの罠・環境差異で詰まるポイントが多い。
// ❌ よくある誤り①:ClientからServerをimport
"use client"
import ServerComponent from '@/components/ServerComponent' // エラー
// ❌ よくある誤り②:Server ComponentでブラウザAPIを使う
export default async function Page() {
console.log(window.location) // サーバーにwindowはない
}
// ❌ よくある誤り③:"use client"を上位に書きすぎる
// → 子コンポーネントがすべてClient Componentになる
// → "use client"は末端コンポーネントのみに限定する
App Routerのキャッシュは4層ある(Request Memoization / Data Cache / Full Route Cache / Router Cache)。
よくある罠:
❌ データを更新したのに画面に反映されない
→ revalidatePath / revalidateTag の呼び忘れ
❌ 開発環境では動くが本番で古いデータが出る
→ 開発環境はキャッシュが効かない設定になっている
❌ ISRで再生成したはずなのに古いHTMLが返る
→ CDN(CloudFront)のキャッシュが残っている
→ Next.jsのキャッシュとCDNのキャッシュは別物
基本の対処法:まず revalidatePath を疑う
// ページ数が多い場合:人気ページのみビルド時に生成
export async function generateStaticParams() {
const posts = await db.getPopularPosts() // 人気記事のみ
return posts.map(p => ({ slug: p.slug }))
}
export const dynamicParams = true // それ以外はリクエスト時に生成
❌ ISRのキャッシュが複数インスタンスで共有されない
ECSで複数タスク起動時、インスタンスAで再生成してもBは古いまま
→ 対策:Redis・S3でキャッシュを共有する
❌ 画像最適化が動かない
→ npm install sharp が必要
❌ Edge Functionsのコードをそのまま移植しようとする
→ Vercel固有・Route Handlerに書き直す
// ❌ 認証チェックなしで使うのは危険
// "use server" の関数は外部から直接呼べる
// ✅ 必ず認証チェックを入れる
"use server"
export async function deletePost(postId: string) {
const session = await getSession()
if (!session) throw new Error('Unauthorized')
await db.posts.delete(postId)
}
💡 ポイント: Next.js v15からキャッシュのデフォルト挙動が変更されており混乱が生じやすい。「まずrevalidatePathを疑う」が基本の対処法。
用語
| 用語 | 説明 |
|---|---|
| CSR | Client-Side Rendering。ブラウザ側でレンダリング |
| SSR | Server-Side Rendering。サーバー側でリクエストのたびにレンダリング |
| SSG | Static Site Generation。ビルド時にHTMLを事前生成 |
| ISR | Incremental Static Regeneration。SSGに定期再生成を加えたもの |
| Hydration | SSR/SSG/ISRで生成した静的HTMLにReactがイベントを後付けする処理 |
| Server Component | デフォルト。サーバーで実行・DBアクセス可・useState不可 |
| Client Component | "use client"で明示。ブラウザで実行・useState可・DBアクセス不可 |
| Server Action | "use server"で定義。クライアントから呼べるサーバー側の関数 |
| revalidatePath | 指定パスのキャッシュを破棄してServer Componentを再実行する関数 |
| stale-while-revalidate | 古いデータを返しながらバックグラウンドで更新するキャッシュ戦略 |
| RSC Payload | App RouterがSSRの差分HTMLをクライアントに返す際の転送形式 |
| TTFBl | Time To First Byte。ブラウザがサーバーから最初のバイトを受け取るまでの時間 |
| LCP | Largest Contentful Paint。最大コンテンツの描画時間。パフォーマンス指標 |