1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SSR・SSG・ISR・PPR —— レンダリング戦略の選び方【Next.js 16版】

1
Posted at

SSR・SSG・ISR・PPR —— レンダリング戦略の選び方【Next.js 16版】

結論から

  • SSG=ビルド時にHTMLを作るSSR=リクエストごとに作る。この2つが基本形で、残りは全部その中間。
  • 迷ったらSSG。SSGで無理なところだけSSRに落とす。 逆順で考えると必ずコストが膨らむ。
  • Next.js 16では「ページ単位で1つ選ぶ」時代が終わった。Cache Componentsを有効にすると、既定の挙動がPPR(Partial Prerendering)——1ページの中で静的な部分と動的な部分を混在させる方式になる。
  • 従来の用語(SSG/SSR/ISR)はもう「ページの属性」ではなく**「そのデータをキャッシュするかどうか」の結果**として決まる。ここが16で一番アタマを切り替えるべきポイント。

1. まず基本の4パターン

方式 HTMLを作るタイミング TTFB SEO 向いているもの
CSR ブラウザ上(JS実行後) 速いが中身は空 弱い 管理画面、ログイン後のダッシュボード
SSG ビルド時に1回 最速(CDN配信) 最強 ブログ、LP、規約ページ
SSR リクエストのたび 遅い(DB待ち) 強い ログイン後の個別ページ、検索結果
ISR ビルド時+一定間隔で再生成 最速 最強 求人一覧、商品ページ、ニュース

サーバーサイド出身者向けに言い換えると:

  • SSG = 静的HTMLを事前に書き出してNginxから配る
  • SSR = LaravelのBladeを毎リクエストレンダリングする
  • ISR = ページキャッシュ + stale-while-revalidate
  • CSR = APIだけ返してVue/Reactで描く

概念自体は目新しくない。Next.jsの価値は「これをページ単位・コンポーネント単位で切り替えられる」ことにある。


2. SSG:デフォルトの第一候補

App Routerでは、リクエスト依存の情報(cookie、header、searchParams)に触らなければ自動的に静的化される

// app/about/page.tsx
// 何も書かなくても、ビルド時にHTMLが生成される
export default function AboutPage() {
  return <h1>会社概要</h1>;
}

外部データを使う場合も同様。

// app/blog/page.tsx
export default async function BlogPage() {
  const posts = await getPosts(); // キャッシュ可能な取得なら静的化される
  return (
    <ul>
      {posts.map((p) => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}

動的ルートのSSG:generateStaticParams

/blog/[slug] のようなURLをビルド時に量産する。Pages Routerの getStaticPaths の後継。

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params; // 16ではparamsが非同期。awaitが必要
  const post = await getPost(slug);
  return <article>{post.body}</article>;
}

paramsPromise になっているのは16の破壊的変更。cookies() / headers() / searchParams も同様に非同期化された。

注意:ページ数が数万を超えるサイトで全件を generateStaticParams に渡すと、ビルドが数十分コースになる。この対策が後述のISRになる。


3. SSR:リクエストごとに作る

リクエスト固有の情報に触った瞬間、そのページは動的になる。

// app/mypage/page.tsx
import { cookies } from 'next/headers';

export default async function MyPage() {
  const cookieStore = await cookies(); // ← ここで動的レンダリング確定
  const token = cookieStore.get('session')?.value;
  const user = await getUser(token);

  return <p>ようこそ、{user.name}さん</p>;
}

明示的に強制することもできる。

export const dynamic = 'force-dynamic';

SSRは「速さを捨てて鮮度を買う」選択。無自覚に使うとTTFBが悪化し、Next.jsを使う旨味が消える。以下のケースに限定したい。

  • ログイン状態で内容が変わる
  • 在庫数・残席数など秒単位の鮮度が要る
  • 検索結果のように組み合わせが無限にある

4. ISR:SSGの弱点を埋める現実解

「静的で配りたいが、コンテンツは日々更新される」——実務で一番多いパターン。

従来型(cacheComponents無効時)

// app/jobs/[id]/page.tsx
export const revalidate = 3600; // 1時間ごとにバックグラウンド再生成

export async function generateStaticParams() {
  const jobs = await getPopularJobs();
  return jobs.map((job) => ({ id: job.id }));
}

更新をトリガーで即座に反映したいときはオンデマンド無効化。

'use server';
import { revalidatePath, revalidateTag } from 'next/cache';

export async function publishJob(id: string) {
  await db.job.update({ where: { id }, data: { published: true } });
  revalidatePath(`/jobs/${id}`);
  revalidateTag('job-list');
}

管理画面から更新 → 該当ページだけ吹き飛ばす、という運用ができる。Laravelの Cache::forget() を叩くのと同じ感覚。

ISRの落とし穴

stale-while-revalidate なので、更新直後の1人目には古いHTMLが返る。 再生成はバックグラウンドで走り、2人目から新しくなる。「更新したのに反映されない」という問い合わせの9割はこれ。オンデマンド無効化を併用するか、仕様として説明しておくこと。


5. Next.js 16の本命:PPR(Partial Prerendering)

ここからが16の新しい話。

従来はページ単位でSSG/SSR/ISRのどれか1つを選ばされた。結果、「ページの99%は静的なのに、ヘッダーのログイン名だけ動的だからページ全体をSSR」という不毛な妥協が発生していた。

PPRはこれを壊す。1ページの中で静的な殻(App Shell)を即座にCDNから返し、動的な穴だけをストリーミングで埋める。

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,     // PPRが既定の挙動になる
  partialPrefetching: true,  // ISR相当の挙動に必要
};

export default nextConfig;
// app/jobs/[id]/page.tsx
import { Suspense } from 'react';

export default async function JobPage({ params }) {
  const { id } = await params;

  return (
    <>
      {/* 静的:ビルド時にHTML化され、CDNから即返る */}
      <JobDetail id={id} />

      {/* 動的:Suspense境界の中で後からストリーミング */}
      <Suspense fallback={<ApplyButtonSkeleton />}>
        <ApplyStatus jobId={id} />
      </Suspense>
    </>
  );
}

async function JobDetail({ id }: { id: string }) {
  'use cache'; // ← 静的な殻に含めてよい、という宣言
  const job = await db.job.findUnique({ where: { id } });
  return <h1>{job.title}</h1>;
}

ルールはシンプル

  1. "use cache" を付けたものは静的な殻に入る
  2. キャッシュされないデータ取得は <Suspense> で包む
  3. 包み忘れるとビルドエラーになる(実行時に黙って遅くなるより遥かにマシ)

3番目が重要で、cacheComponents: true への移行が「フラグを立てるだけ」で済まない理由がここにある。既存アプリなら段階的にやること。

大量ページ × ISR の新しい書き方

16では generateStaticParams全件を渡さなくてよくなった。人気ページだけ事前生成し、残りは初回アクセス時にApp Shellを即返してバックグラウンドで完成させる。

export async function generateStaticParams() {
  const popular = await getPopularJobs(100);
  return popular.map((job) => ({ id: job.id })); // 100件だけ事前生成
}

未指定のURLも普通に配信され、初回訪問後にキャッシュへ昇格する。Pages Routerの fallback: true に相当する挙動が既定になったイメージ。router.isFallback は不要。

なお Cache Components有効時、generateStaticParams は最低1件を返す必要がある。空配列はビルドエラー。


6. SEOで踏みやすい罠

PPRでは、ボット・クローラーはUser-Agentで判定され、殻ではなく完全なHTMLを受け取る(レンダリング完了を待ってから返す)。人間には速く、クローラーには完全なHTMLを、という設計。

ここに罠がある。殻の生成がビルド時にしか存在しない値に依存していると、人間には表示できてクローラーにはエラー、という状態が起こりうる。求人サイトのようにSEOが売上に直結する案件では、リリース前に必ずクローラーUAで実際に叩いて確認すること。

curl -A "Googlebot" https://example.com/jobs/123 | head -50

7. 選択フローチャート

そのページはログイン必須か?
├─ Yes → SSR(またはCSRでAPI取得)
└─ No
   └─ コンテンツは更新されるか?
      ├─ ほぼ不変(規約・LP) → SSG
      └─ 更新される
         └─ 秒単位の鮮度が要るか?
            ├─ Yes(在庫・残席) → PPRで該当箇所だけ動的に
            └─ No(記事・求人) → ISR(+ オンデマンド無効化)

現実の実装では、1ページの中でこれが混在する。 「このページはSSR」ではなく「このコンポーネントのこのデータはキャッシュする / しない」という粒度で考えるのが16以降の正解。


8. 移行するなら

既存のNext.js 15アプリを16のCache Componentsに移すときの順番。

  1. まず16へ上げる(npx @next/codemod@canary upgrade latest)。この時点では cacheComponents は入れない
  2. params / cookies() / headers() の非同期化に対応してビルドを通す
  3. next build --webpack で通ることを確認してからTurbopackに切り替え
  4. 最後に cacheComponents: true。ビルドエラーを潰しながら "use cache"<Suspense> を足していく

4番目だけで数日〜数週間かかる規模もある。まとめてやらないこと。


参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?