Next.js 16におけるSSR対React Server Components:本番環境向けの意思決定ガイド

バージョン9からNext.jsアプリを配信してきました。当時はgetServerSidePropsが最新の機能でした。過去1年間、3つの大規模本番アプリケーションをNext.js 16のApp Routerに移行し、SSRとReact Server Componentsをいつ使うかについて、あらゆる間違った意思決定をしてきました。このガイドは、それらの移行を始める前に欲しかったドキュメントです。

SSR対RSCについての会話は、ハイプ、不完全なメンタルモデル、そして正直なところ、いくつかの混乱を招くドキュメントで曇っています。それらは競合するテクノロジーではなく、アプリケーションの異なるレイヤーで異なる問題を解決する相補的なツールです。しかし、特定のシナリオでどのツールを使うべきかを知ること?これが本当のエンジニアリング判断が存在する場所です。

実際の本番データ、実際のコードパターン、カンファレンストークでは話されない権衡を含めて、学んだすべてをご紹介します。

目次

SSR vs RSC in Next.js 16: A Production Decision Guide

基礎の理解

詳細に入る前に、きれいなメンタルモデルを確立しましょう。これは思うより重要です。SSRとRSCを混同したシニアエンジニアを見たことがあります。用語が重なるためです。

*Server Side Rendering(SSR)*レンダリング戦略*です。これはいつそしてどこ*でコンポーネントツリーがHTMLに変換されるかを決定します。SSRでは、すべてのリクエストがサーバーに到達し、完全なコンポーネントツリーをHTMLにレンダリングし、クライアントに送信し、その後React全体のツリーをハイドレートしてインタラクティブにします。

*React Server Components(RSC)*コンポーネントタイプ*です。これは何が*クライアントに送信されるかを決定します。Server Componentsはサーバーで実行され、その렌더링출력(HTMLではなく、シリアル化されたReactツリーとして)をクライアントに送信します。ハイドレートされません。ブラウザにJavaScriptを送信しません。

違いがわかりますか?SSRはレンダリングのタイミングについてです。RSCはコンポーネント境界と何がどこに送信されるかについてです。

Next.js 16.2でApp Routerを使用している場合、実は両方を同時に使用しています。すべてのページリクエストは、Server ComponentsとClient Componentsの両方を含むコンポーネントツリーのサーバー側レンダリングを伴います。RSCレイヤーはどのコンポーネントがハイドレーションJavaScriptを必要とするかを決定し、SSRレイヤーはHTMLがどのように生成されるかを決定します。

コンポジションモデル

ここが重要な洞察です。App Routerでは、Server Componentsがデフォルトであるということにしばらく気づきませんでした。'use client'でクライアント動作を選択します。これはPages Routerの古いモデルを逆にします。

// これはApp RouterでデフォルトのServer Componentです
// このコンポーネントのJavaScriptはブラウザに送信されません
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({ where: { id: params.id } });
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* このClient Componentアイランドは独立してハイドレートされます */}
      <AddToCartButton productId={product.id} price={product.price} />
    </div>
  );
}
// components/AddToCartButton.tsx
'use client';

import { useState } from 'react';

export function AddToCartButton({ productId, price }: Props) {
  const [loading, setLoading] = useState(false);
  // このコンポーネントのJSだけがブラウザに送信されます
  return <button onClick={handleAdd}>Add to Cart — ${price}</button>;
}

Next.js 16でのSSRの仕組み

App RouterのSSRはPages RouterのgetServerSidePropsと同じものではありません。実行モデルは根本的に変わっています。

Next.js 16では、dynamic = 'force-dynamic'を設定するか、Server Componentでcookies()headers()、またはsearchParamsを使用する場合、Next.jsに次のように伝えています:「このページは静的に生成できません。すべてのリクエストで新しくレンダリングしてください。」

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

export const dynamic = 'force-dynamic';

export default async function Dashboard() {
  const session = await cookies();
  const userId = session.get('userId')?.value;
  const data = await fetchDashboardData(userId);
  
  return <DashboardLayout data={data} />;
}

レンダリングパイプラインは以下のようになります:

  1. リクエストはサーバーに到達します
  2. Next.jsはRSCツリーをトップダウンで実行します
  3. Server Componentsはそれらの非同期操作(データ取得など)を解決します
  4. レンダリングされたRSCペイロードはシリアル化されます
  5. SSRはこれをHTMLに初期応答に変換します
  6. クライアントはHTML + RSCペイロード + Client Component JSを受け取ります
  7. ReactはClient Component境界のみをハイドレートします

ステップ3~6はストリーミング経由で発生する可能性があります。これについては下で詳しく説明します。

React Server Componentsの仕組み

RSCsは単に「サーバー上で実行されるコンポーネント」ではありません。根本的に異なる実行モデルを表しています。

Server Componentがレンダリングされると、その出力はUIのシリアル化された説明です。JSON のようなツリー構造に似ています。このペイロードには、Server Componentsのレンダリング出力(HTMLのようなノードとして)と、Client Componentsへの参照(モジュールポインタプラスシリアル化されたpropsとして)が含まれます。

これは以下を意味します:

  • Server Componentsはデータベース、ファイルシステム、サーバーのみのAPIに直接アクセスできます
  • コンポーネントレベルでasync/awaitを使用できます
  • それらのコード、依存関係、インポートはクライアントバンドルに決して表示されません
  • useStateuseEffect、またはブラウザAPIを使用できません
  • Client Componentsへのpropsとして関数を渡すことはできません(関数はシリアル化できません)

最後のポイントは常に人々を困惑させます。これはできません:

// ❌ これはエラーをスローします
async function ServerParent() {
  const handleClick = () => console.log('clicked');
  return <ClientChild onClick={handleClick} />;
}

ハンドラーをClient Component自体に移動するか、Server Actionsを使用する必要があります。

SSR vs RSC in Next.js 16: A Production Decision Guide - architecture

パフォーマンス比較:実際の本番データ

Pages Router(従来のSSR)からNext.js 16.2のApp Router(RSC + SSR)への移行中に、3つの本番アプリケーション全体で制御されたベンチマークを実行しました。以下が実際の数値です。

テスト環境

  • AWS us-east-1、t3.xlargeインスタンス
  • PrismaベースのPostgreSQL、Redisキャッシュレイヤー
  • Web Vitals RUMデータで測定、30日間のウィンドウ
  • 3つのアプリ全体で月間約230万ページビュー
メトリクス Pages Router(SSR) App Router(RSC) デルタ
TTFB(p50) 320ms 180ms -43.7%
TTFB(p95) 890ms 410ms -53.9%
FCP(p50) 1.2s 0.8s -33.3%
LCP(p50) 2.1s 1.4s -33.3%
TTI(p50) 3.8s 1.9s -50.0%
INP(p75) 180ms 95ms -47.2%
転送されたJavaScript合計 387KB 142KB -63.3%
ハイドレーション時間(p50) 450ms 120ms -73.3%

TTIとハイドレーション時間の改善がメインの数値です。コンポーネントツリーの70%のコンポーネントJavaScriptの送信を停止すると、ブラウザにはやることがはるかに少なくなります。

しかし、ここで微妙な点があります:TTFBが改善されたのはRSC自体が原因ではなく、ストリーミングが原因です。App Routerはhtml応答をストリーミングするため、ブラウザはページ全体がレンダリングされる前にバイトを受け取り始めます。Pages Routerでは、getServerSidePropsがHTMLを送信する前に完全に完了する必要がありました。

バンドルサイズへの影響

これはRSCが最も輝く場所であり、最も誤解が多い場所です。

従来のSSR設定では、すべてのコンポーネントはハイドレーション用のJavaScriptをクライアントに送信します。コンポーネントが対話的な何もしない場合でも。考えてみてください:製品説明、ブログ投稿本文、フッターナビゲーション。すべてのレンダリングロジックは、Reactが「ハイドレート」し、サーバーHTMLが一致することを確認するために、ブラウザに送信されます。

RSCを使用すると、これらのコンポーネントはJavaScriptを送信しません。

e-コマースクライアントの1つについて、バンドルの構成は以下の通りです:

コンポーネントカテゴリ Pages Routerバンドル App Routerバンドル 節約
レイアウト/Chrome 45KB 0KB(Server Component) 100%
製品表示 38KB 0KB(Server Component) 100%
ナビゲーション 22KB 8KB(インタラクティブパーツのみ) 63.6%
検索 31KB 28KB(ほぼクライアント) 9.7%
カート/チェックアウト 67KB 62KB(ほぼクライアント) 7.5%
サードパーティライブラリ 184KB 44KB 76.1%
合計 387KB 142KB 63.3%

そのサードパーティライブラリの行は巨大です。date-fnsmarkedsanitize-htmlなどのライブラリ。Server Componentでのみ使用される場合、クライアントバンドルへのゼロコストです。sharpを使用してServer Componentでの画像処理を行うページがありました。これは1.2MBのライブラリであり、ブラウザはそれについて何も知りません。

ストリーミングとウォーターフォールパターン

ストリーミングはApp Routerの秘密兵器であり、データ取得のウォーターフォールについての考え方を根本的に変えます。

古いウォーターフォール問題

Pages Router SSR:

Request → getServerSideProps(すべてのデータ) → Render → Send HTML → Download JS → Hydrate
         |__________ 800ms ___________|   200ms   |__ 0ms __|__ 300ms __|__ 450ms __|

すべてが初期データ取得でブロックされます。3つのAPIからデータが必要な場合、それらはgetServerSidePropsで並列に実行されるか、ウォーターフォールがあります。

SuspenseでのStreaming

App Router RSC:

Request → Render shell → Stream HTML(即座) → Stream data sections → Download JS → Hydrate(部分的)
         |__ 50ms __|    |_____ 0ms _____|       |____ 進行中 ____|   |_ 並列 _|__ 120ms __|

重要な違い:ブラウザはHTMLの受信を直ちに開始します。Suspense境界は、ページのどの部分が準備ができたら流れるかを定義します。

import { Suspense } from 'react';

export default function ProductPage({ params }) {
  return (
    <div>
      {/* 即座に送信されます */}
      <Header />
      <ProductHero productId={params.id} />
      
      {/* 準備ができたら流れます */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>
      
      {/* 独立して流れます */}
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={params.id} />
      </Suspense>
    </div>
  );
}

Suspense境界は独立してストリーミングされます。レコメンデーションに2秒かかるがレビューに200msかかる場合、レビューが最初に表示されます。ユーザーは空白の画面またはフルスケルトンの代わりに、プログレッシブなコンテンツロードを見ます。

新しいウォーターフォールの回避

しかし、RSCは独自のウォーターフォールリスクを導入します。親子server componentのデータ取得は、シーケンシャルウォーターフォールを作成できます:

// ❌ シーケンシャルウォーターフォール
async function Parent() {
  const user = await getUser(); // 200ms
  return <Child userId={user.id} />; // Parentが解決されるまで開始できません
}

async function Child({ userId }) {
  const orders = await getOrders(userId); // 300ms
  return <OrderList orders={orders} />;
}
// 合計:500ms

修正は、データ取得をできるだけ深くプッシュし、並列フェッチングパターンを使用することです:

// ✅ Suspenseでの並列
async function Parent() {
  const userPromise = getUser();
  return (
    <>
      <Suspense fallback={<UserSkeleton />}>
        <UserProfile promise={userPromise} />
      </Suspense>
      <Suspense fallback={<OrdersSkeleton />}>
        <UserOrders promise={userPromise} />
      </Suspense>
    </>
  );
}

実際に機能するキャッシング戦略

Next.js 16はバージョン14と15での複雑さについてコミュニティが(当然のことながら)不満を述べた後、キャッシングを大幅に見直しました。現在のモデルがどのようなものであり、SSRとRSCがどのように機能するかを見てみましょう。

`fetch`を使用したリクエストレベルキャッシング

fetchを使用するServer Componentsは、リクエストごとにキャッシング設定できます:

// 60秒間キャッシュ(ISR動作)
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 60 }
});

// キャッシュなし、すべてのリクエストで新規(SSR動作)
const data = await fetch('https://api.example.com/user/profile', {
  cache: 'no-store'
});

// オンデマンド再検証用のタグでキャッシュ
const data = await fetch('https://api.example.com/products/123', {
  next: { tags: ['product-123'] }
});

セグメントレベルのキャッシング

単一ページ内でレンダリング戦略を混ぜることができます:

// 静的レイアウト(ビルド時にキャッシュ)
export default function Layout({ children }) {
  return <div><Nav />{children}<Footer /></div>;
}

// 動的ページ(すべてのリクエストで新規)
export const dynamic = 'force-dynamic';
export default async function Page() { /* ... */ }

キャッシングが複雑になる場合

本当のGotcha:ルートセグメント内の任意のコンポーネントが動的関数(cookies()headers()searchParams)を使用する場合、セグメント全体が動的になります。深くネストされたServer Component内の1つのキャッシュされていないフェッチが、ページ全体を動的にします。

これは本番環境で問題になりました。ISRキャッシュされるはずだった製品ページがありましたが、深くネストされたRecentlyViewedコンポーネントがクッキーを読み込んでいました。ページ全体が動的になり、TTFBが50msから400msに上がり、2週間気づかなかった。

修正:動的コンポーネントをSuspense境界の後ろに分離するか、クライアント側でフェッチするClient Componentsに移動します。

意思決定フレームワーク:各々をいつ使うか

3つの本番アプリの移行後、これが私が使う意思決定フレームワークです。「SSR対RSC」というより「どのコンポーネントにどのレンダリング戦略」についてです。

Server Components(デフォルト)を使用する場合:

  • コンポーネントがデータを表示するが、インタラクティビティは必要ない
  • サーバーのみのリソース(DB、ファイルシステム、プライベートAPI)を使用している
  • コンポーネントが重いライブラリをインポート(マークダウンパーサー、構文ハイライター)
  • コンテンツのSEOが重要(検索エンジンは完全なHTMLを取得)
  • コンテンツを静的に分析またはキャッシュできる

Client Componentsを使用する場合:

  • useStateuseEffectuseRef、またはその他のReactフックが必要
  • ブラウザAPI(localStorage、地理的位置情報、IntersectionObserver)が必要
  • イベントハンドラー(onClick、onChange、onSubmit)が必要
  • ブラウザコンテキストを必要とするサードパーティライブラリを使用している
  • リアルタイム更新(WebSocket、ポーリング)が必要

SSR(force-dynamic)を使用する場合:

  • コンテンツはユーザー/セッションごとにパーソナライズされている
  • データが変更されすぎてISRに合わない
  • リクエスト時間の情報が必要(認証状態、geo-locationヘッダー)
  • SEOには引き続きサーバーレンダリングHTMLが必要

静的生成を使用する場合:

  • コンテンツがめったに変わらない(マーケティングページ、ドキュメント、ブログ投稿)
  • パフォーマンスが重要(CDNエッジでキャッシュ)
  • コンテンツはすべてのユーザーで同じ

Next.jsの開発プロジェクトでは、通常大体このスプリットで終わります:60% Server Components(静的)、20% Server Components(動的/SSR)、15% Client Components、5%はSuspense境界を使った混合パターン。

Pages Routerからの移行パターン

既存のNext.jsアプリを移行している場合、すべてを一度に変換しようとしないでください。これは壊滅的に失敗するのを見ました。以下は、機能する増分アプローチです:

フェーズ1:共存

Next.js 16はpages/app/ディレクトリの両方を同時にサポートしています。新しいルートをapp/で開始し、既存のものはそのままにします。

フェーズ2:レイアウト移行

最初にレイアウトを移動してください。_app.tsx_document.tsxapp/layout.tsxになります。これは通常最も簡単なのです。レイアウトは完璧なServer Componentです。

フェーズ3:最初に静的ページ

最も単純な静的ページを移行します。マーケティングページ、アバウトページ、ブログ投稿。これらは直前のServer Component変換です。

フェーズ4:動的ページ

getServerSidePropsを使用するページを変換します。これは最も摩擦が多い場所です。特にデータフェッチングパターンと認証の周囲。

フェーズ5:クライアント対話性

インタラクティブなアイランドをClient Componentsに抽出します。これは最も難しい部分です。最小クライアント境界を特定する必要があります。

// Before:Pages Routerではすべてが「クライアント」がデフォルトでした
// After:明示的な境界

// app/products/[id]/page.tsx (Server Component)
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);
  return (
    <article>
      <h1>{product.name}</h1>
      <ProductGallery images={product.images} /> {/* Client */}
      <div dangerouslySetInnerHTML={{ __html: product.description }} /> {/* Server */}
      <PricingWidget product={product} /> {/* Client */}
      <Suspense fallback={<Skeleton />}>
        <RelatedProducts categoryId={product.categoryId} /> {/* Server */}
      </Suspense>
    </article>
  );
}

移行戦略の計画に助けが必要な場合、私たちのチームはこれを十分に何度もしているため、地雷がどこにあるかを知っています。お問い合わせください。特定のアーキテクチャについて話すことができます。

技術的なSEOへの影響

JavaScriptレンダリングをどのように検索エンジンが処理するかを12年以上見守ってきた中で、言えることは:RSCモデルはSSR自体以来のテクニカルSEOにとって最高のものです。

理由は以下の通りです:

Server Componentsはサーバー上で完全なHTMLをレンダリングします。 Googlebotはどのジャバスクリプトも実行せずに完全なコンテンツを取得します。これは新しくありません。SSRもこれをしました。しかし、RSCsはドラマティックに少ないクライアント側JavaScriptでそれを行い、これはCore Web Vitalsに直接影響します。

Googleは、INP(相互作用から次のペイントまで)が2024年3月の時点でランキング信号であることを確認しました。本番データは、RSC集約的なページがSSRページより同等のINPで47%スコアが高いことを示しています。JavaScriptが少ない = メインスレッド競合が少ない = より良いINP。

ストリーミングはクロール動作に影響します。 Googlebotは2023年の時点でHTTPストリーミングをサポートしていますが、タイムアウトがあります。最も遅いSuspense境界に15秒かかる場合、Googlebotはそれを待たないかもしれません。重要なSEOコンテンツをSuspense境界の外に保つか、またはフォールバックに意味のあるコンテンツを確保してください。

SEOが主な関心事であるクライアントの場合、headless CMS開発アプローチとApp Routerのペアリングを推奨します。コンテンツはCMSに存在し、Server Componentsを経由してレンダリングされ、不要なJavaScriptをブラウザに送信します。すべてのベストワールド。

Astroも検討する価値があります。サイトが主にコンテンツドリブンで、インタラクティビティが最小限の場合。しかし、リッチなインタラクティブ機能を持つアプリケーションの場合、Next.js 16 RSCでスイートスポットにヒットします。

FAQ

Next.js 16でのSSRとRSCの違いは何ですか? SSR(Server Side Rendering)はレンダリング戦略であり、ページHTMLがいつ生成されるかを決定します。すべてのリクエストで、サーバーで。React Server Components(RSC)はコンポーネントタイプであり、どのコードがブラウザに送信されるかを決定します。App Routerでは、それらが一緒に機能します:RSCsはどのコンポーネントがクライアントJavaScriptを必要とするかを定義し、SSRはHTMLがどのように生成されるかを処理します。通常、両方を同時に使用しています。

React Server ComponentsはServer Side Renderingを置き換えますか? いいえ。RSCsとSSRは競合ではなく、補完的です。Next.js 16のApp Routerでは、すべてのページは初期HTML応答にSSRを使用します。RSCsはそのページ内のどのコンポーネントがハイドレーション用にクライアントに送信するJavaScriptを必要とするかを決定します。完全にSSR'd なページを完全にServer Components(クライアントJSなし)で構成するか、両方を混ぜることができます。

React Server Componentsはどのくらいバンドルサイズを削減しますか? 本番測定では、RSCベースのApp Routerページは平均して同等のPages Router実装と比較して63%小さいJavaScriptバンドルでした。節約はコンポーネントツリーに大きく依存します。ディスプレイ専用コンテンツの多いページは最大の利益を見て、高度にインタラクティブなページ(ダッシュボード、エディター)はより小さな改善を見ます。

既存のNext.jsアプリをApp Routerに移行すべきですか? これはあなたの問題点に依存します。Core Web Vitalsが大きなJavaScriptバンドルのため低下していたり、TTFBがシーケンシャルデータフェッチのため高い場合、移行は価値があります。Pages Router アプリが良く実行され、チームが生産的な場合、急いでする必要はありません。Next.jsは両方のルータを同時にサポートするため、段階的に移行できます。

Next.js 16でServer Componentsのキャッシングはどのように機能しますか? Next.js 16は大幅にキャッシングモデルを単純化しました。Server Componentsは静的にキャッシュでき(静的データの場合のデフォルト)、ベースに再検証でき(ISR)、またはすべてのリクエスト時に新しくレンダリングできます(動的)。フェッチレベルでnext: { revalidate }を使用するか、ルートセグメントレベルでexport const dynamicを使用して制御します。注意:セグメント内の1つの動的関数は、セグメント全体を動的にします。

Server ComponentsはSEOに影響を与えますか? Server Componentsはすばらしいです。JavaScriptを実行せずにサーバー上で完全なHTMLをレンダリングするため、検索エンジンはインデックスできます。さらに、クライアント側JavaScriptの削減は、Core Web Vitalsスコアを改善します。特にINPとTTI。ランキング信号です。1つの注意点は、Suspense境界内のコンテンツが段階的にストリーミングされるため、重要なSEOコンテンツが遅いデータフェッチの後ろにないことを確認してください。

headless CMSでReact Server Componentsを使用できますか? 絶対に。これは最高のペアリングの1つです。Server ComponentsはコンポーネントレベルでCMSコンテンツを直接フェッチでき、APIキーやCMS SDKコードをクライアントに公開することなく。@prismicio/clientのようなContentful SDK、Sanityclient、またはPrismic's SDKは完全にサーバー上にあります。WebhooksでISRまたはオンデマンド再検証を組み合わせると、高速でキャッシュ可能なページが不要なクライアントJavaScriptでゼロになります。

本番環境でRSCを使用する際の最大の落とし穴は何ですか? 私が提起した3つの最大の問題:(1)ネストされたServer Componentsの偶発的なウォーターフォールデータ取得。React DevToolsとサーバータイミングヘッダーでプロファイルと修正します。(2)ネストされたコンポーネント内でcookies()またはheaders()を使用してキャッシュされたページを誤って動的にする。(3)NonSerializable データ(関数、classインスタンス、日付)をServer ComponentからClient Componentに渡すときのpropシリアライズエラー。早期に良いリンティング規則とコンポーネント境界規約を構築してください。