😭

SSRとRSCの違いが分からなかったが、少し理解。。。

に公開3

はじめに

Next.jsのApp Routerを触っていると、「SSR」「RSC」「Server Component」といった用語が飛び交います。はっはっはって感じです笑。

まず、大前提に念のための確認ですが、

RSC = React Server Components = Server Component です♪
これだけでもちょっと腹立ってます笑。最初は、え?なんか違うん??
これはReactの機能??Next.jsの機能??どっち??とかっていう疑問点も出てきます。

そして、さらには、当然、最初は以下のような疑問が出ます!(私だけ?)

「Server Componentって、サーバーで動いてるんでしょ?それってSSRじゃないの?何が違うん?」

私の頭の中の初期状態は完全にこの状態でした。
この記事では、この疑問に対して、実際のコードを見ながら、少しずつ違いを整理できたらと思います。

結論:Server Component ≠ SSR

まず結論から言います。

Server ComponentとSSRは、別物です。 はい、、当然ですよね、、、。

ただし、App RouterではServer ComponentとSSRが"セット"で動いているため、
区別がつきにくいと勝手に思っています。

SSRには「2つの意味」がある

混乱の最大の原因は、SSRという言葉が2つの意味で使われていることです。

広い意味のSSR(戦略・アーキテクチャ)

「サーバーサイドでレンダリングする」という全体の方針のこと。

これは「CSR(Client Side Rendering)の対義語」として使われます。

狭い意味のSSR(具体的な処理)

「ReactコンポーネントをサーバーでHTMLに変換する処理」のこと。

renderToString()のような関数を使ってHTMLを吐き出す処理を指します。

この記事では、以降「SSR」と書いた場合は**狭い意味のSSR(HTML生成処理)**を指すことにします。

Server Component(RSC)とは何か

Server Componentは、サーバーでしか実行されないReactコンポーネントです。

重要なのは、Server Componentは「HTML」を直接作らないということ。

では何を作るかというと、「RSC Payload」という特殊なデータ構造を作ります。

RSC Payloadには以下が含まれます。

  • コンポーネントのツリー構造
  • データ
  • Client Componentへの参照

これは、JSON風のデータだと思います。

App Routerでの実際の流れ

ユーザーがページにアクセスしたとき、以下の順で処理が進みます。

  1. Server Componentが実行される(RSC)
    データ取得、計算を行い、RSC Payloadを生成

  2. SSRが実行される(狭い意味のSSR)
    RSC PayloadをもとにHTMLを生成

  3. ブラウザにHTMLとJSが送られる
    HTMLは即座に表示され、JSでハイドレーションが行われる

つまり、**RSCは「材料を作る工程」、SSRは「HTMLに変換する工程」**なのです。

具体例で比較:Pages Router vs App Router

Pages Router(昔のSSR)

// pages/index.js
export default function Page({ data }) {
  return (
    <div>
      <h1>{data.title}</h1>
      <Counter />
    </div>
  );
}

export async function getServerSideProps() {
  const data = await fetch('...');
  return { props: { data } };
}

SSRの結果として生成されるHTML

<div>
  <h1>記事タイトル</h1>
  <button>0</button>
</div>

App Router(今のRSC + SSR)

// app/page.js(Server Component)
export default async function Page() {
  const data = await fetch('...'); // 直接書ける
  return (
    <div>
      <h1>{data.title}</h1>
      <Counter />
    </div>
  );
}
// components/Counter.js
'use client';

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

一見、同じようなHTMLが生成されます。

<div>
  <h1>記事タイトル</h1>
  <button>0</button>
</div>

では何が違うのでしょうか?

決定的な違い:何がブラウザに送られるか

Pages Routerの場合

ブラウザに送られるもの

HTML

<div>
  <h1>記事タイトル</h1>
  <button>0</button>
</div>

JavaScript

  • PageコンポーネントのJS
  • CounterコンポーネントのJS
  • その他すべてのコンポーネントのJS

App Routerの場合

ブラウザに送られるもの

HTML

<div>
  <h1>記事タイトル</h1>
  <button>0</button>
</div>

JavaScript

  • CounterコンポーネントのJSだけ

Server Component(Page)のJSはブラウザに送られません。

Pages Routerの問題点:二重送信

Pages RouterのSSRでは、同じコンポーネントの情報を2つの形式で送っているのです。

HTMLとして送る

サーバーが最初に返すHTMLの中に、Counterの見た目が入っています。

<button>0</button>

ブラウザはこれを受け取って、即座に画面に表示します。

でも、この時点ではボタンは動きません。クリックしても何も起きません。

なぜなら、JavaScriptがまだ実行されていないからです。

JavaScriptとして送る

同時に、ブラウザには**Counterコンポーネントのコード(JS)**も送られています。

function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

このJSが読み込まれて実行されると、ハイドレーションが起きます。

ハイドレーションとは、すでに表示されているHTML(ボタン)に、JSの機能(クリックイベント)を紐付ける処理のことです。

これで初めて、ボタンがクリックできるようになります。

なぜ「二重」なのか

Counterコンポーネント1つに対して、以下の2つが送られます。

形式 内容 サイズ 役割
HTML <button>0</button> 小さい 初期表示(見た目)
JavaScript function Counter() { ... } 大きい インタラクション(動き)

同じコンポーネントの情報が、2つの形式で送られています。これが「二重送信」です。

無駄を削減:RSCの真価

例えば、こんなページがあったとします。

// pages/index.js
export default function Page({ posts }) {
  return (
    <div>
      <Header />
      <PostList posts={posts} />
      <Counter />
      <Footer />
    </div>
  );
}

よく考えてください。

  • HeaderPostListFooterは、表示するだけで、クリックもできないし、状態も変わらない
  • Counterだけが、クリックで状態が変わる(インタラクティブ)

表示だけのコンポーネントのJSまでブラウザに送る必要があるでしょうか?

Pages Router(SSR)の場合

ブラウザに送られるJS

  • Header.js
  • PostList.js
  • Counter.js
  • Footer.js

全部のコンポーネントのJS = 例えば100KB

App Router(RSC)の場合

// app/page.js(Server Component)
export default async function Page() {
  const posts = await fetch('...');
  return (
    <div>
      <Header />
      <PostList posts={posts} />
      <Counter />
      <Footer />
    </div>
  );
}

この場合、以下のようになります。

コンポーネント HTMLとして送る? JSとして送る?
Header はい いいえ(JSは送らない)
PostList はい いいえ(JSは送らない)
Counter はい はい(クライアントコンポーネントだから)
Footer はい いいえ(JSは送らない)

ブラウザに送られるJS

  • Counter.jsだけ

CounterだけのJS = 例えば20KB

80KBの削減!ページの読み込みが速くなります。

Server Componentのコードは「実行結果」だけが送られる

もう一つ重要なポイントがあります。

// app/page.js(Server Component)
export default async function Page() {
  const data = await fetch('...'); // ①この処理はサーバーで実行
  return (
    <div>
      <h1>{data.title}</h1> {/* ②実行結果がブラウザに送られる */}
      <Counter /> {/* ③Client ComponentはJSも送られる */}
    </div>
  );
}

const data = await fetch('...');

この処理はサーバーでのみ実行されて、ブラウザには送られません。

<h1>{data.title}</h1>

この部分は、「コード」としてはブラウザに送られません。

でも、「実行結果」はブラウザに送られます。

サーバーでdata.title"Next.jsの記事"だったとします。

ブラウザに送られるのは、<h1>{data.title}</h1>というコードではなく、<h1>Next.jsの記事</h1>という実行済みのHTMLです。

<Counter />(Client Component)

Client Componentなので、以下の両方が送られます。

  • HTML(初期状態のHTML:<button>0</button>
  • JavaScript(Counterコンポーネントのコード)

表で整理

コード サーバーで実行? ブラウザに送られるもの
const data = await fetch('...'); はい 何も送られない(処理だけ)
<h1>{data.title}</h1> はい(data.titleを埋め込む) 実行結果のHTML<h1>Next.jsの記事</h1>
<Counter /> はい(初期HTMLを作る) HTML + JS<button>0</button> + Counter.js

つまり、Server Componentのreturn文の中は、「コード」ではなく「実行結果」がブラウザに送られます。

{data.title}というコードはブラウザには送られず、"Next.jsの記事"という値が送られます。

だから、Server ComponentのJSコードはブラウザに送られない = 軽量化できるのです。

RSC Payloadとは何か

ここまでで「HTMLは送られる」と説明してきましたが、実はもう一つ重要なものが送られています。

それがRSC Payloadです。

HTMLとRSC Payloadの違い

HTML

ブラウザがそのまま表示できる形式です。

<div>
  <h1>Hello</h1>
  <p>World</p>
</div>

これは最終形態です。ブラウザはこれを見て、画面に描画します。

RSC Payload

Reactが理解できる中間データです。HTMLではありません。

イメージとしては、こんな感じです。

{
  type: "div",
  children: [
    { type: "h1", children: "Hello" },
    { type: "p", children: "World" },
    { type: "ClientComponent", id: "Counter" }
  ]
}

これはHTMLではなく、Reactのコンポーネントツリーの設計図です。

なぜHTMLだけではダメなのか

重要なポイントは、RSC PayloadにはClient Componentの「場所」が記録されていることです。

  • Server Componentの部分は、すでにデータ化されている
  • Client Componentの部分は、「ここにこのコンポーネントが入るよ」という印だけが入っている

そして、**ブラウザ側でJSが動いたときに、Client Componentがその場所に「はめ込まれる」**のです。

さらに、RSC Payloadがあるから、画面遷移(ナビゲーション)のときに、サーバーから新しいデータだけを取得して、部分的に更新できるのです。

まとめ:SSRとRSCの関係

Pages Router(昔)

ユーザーがアクセス

getServerSideProps実行(データ取得)

SSR:コンポーネント全体をHTMLに変換

ブラウザに送信
├─ HTML(全コンポーネント)
└─ JS(全コンポーネント)← 無駄が多い

App Router(今)

ユーザーがアクセス

Server Component実行(データ取得 + RSC Payload生成)

SSR:RSC PayloadをもとにHTMLに変換

ブラウザに送信
├─ HTML(全コンポーネント)
├─ RSC Payload(ツリー構造)
└─ JS(Client Componentのみ)← 軽量化

図解:全体像

SSRとRSCは別物だが、セットで動く

  1. RSC(Server Component) は、サーバーでデータを処理してRSC Payloadを作る「材料作り」
  2. SSR は、RSC PayloadをHTMLに変換する「最終調理」
  3. Server ComponentのJSはブラウザに送られないため、軽量化される
  4. Client ComponentのJSだけがブラウザに送られ、必要な部分だけインタラクティブになる

SSRとRSCの違い(表で整理)

SSR(狭い意味) RSC
何を作る? HTML RSC Payload(データ)
いつ動く? 初回リクエスト時 毎回(ナビゲーション時も)
JSは送る? Client Component分は送る Server Component分は送らない

最後に

この記事で、SSRとRSCの違いが少しでも明確になれば幸いです。

重要なのは、以下の3点です。

  1. SSRは「HTMLを作る処理」、RSCは「サーバーで動くコンポーネント」
  2. Server ComponentのJSはブラウザに送られない = 軽量化
  3. Client ComponentのJSだけが送られる = 必要な部分だけインタラクティブ

この理解があれば、Next.jsのApp Routerがなぜ速いのか、なぜServer ComponentとClient Componentを使い分けるのかが見えてくるはずです。私は少し見えました笑。
引き続き、学習頑張ります🔥

Discussion