SSRとRSCの違いが分からなかったが、少し理解。。。
はじめに
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での実際の流れ
ユーザーがページにアクセスしたとき、以下の順で処理が進みます。
-
Server Componentが実行される(RSC)
データ取得、計算を行い、RSC Payloadを生成 -
SSRが実行される(狭い意味のSSR)
RSC PayloadをもとにHTMLを生成 -
ブラウザに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>
);
}
よく考えてください。
-
Header、PostList、Footerは、表示するだけで、クリックもできないし、状態も変わらない -
Counterだけが、クリックで状態が変わる(インタラクティブ)
表示だけのコンポーネントのJSまでブラウザに送る必要があるでしょうか?
Pages Router(SSR)の場合
ブラウザに送られるJS
Header.jsPostList.jsCounter.jsFooter.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は別物だが、セットで動く
- RSC(Server Component) は、サーバーでデータを処理してRSC Payloadを作る「材料作り」
- SSR は、RSC PayloadをHTMLに変換する「最終調理」
- Server ComponentのJSはブラウザに送られないため、軽量化される
- Client ComponentのJSだけがブラウザに送られ、必要な部分だけインタラクティブになる
SSRとRSCの違い(表で整理)
| SSR(狭い意味) | RSC | |
|---|---|---|
| 何を作る? | HTML | RSC Payload(データ) |
| いつ動く? | 初回リクエスト時 | 毎回(ナビゲーション時も) |
| JSは送る? | Client Component分は送る | Server Component分は送らない |
最後に
この記事で、SSRとRSCの違いが少しでも明確になれば幸いです。
重要なのは、以下の3点です。
- SSRは「HTMLを作る処理」、RSCは「サーバーで動くコンポーネント」
- Server ComponentのJSはブラウザに送られない = 軽量化
- Client ComponentのJSだけが送られる = 必要な部分だけインタラクティブ
この理解があれば、Next.jsのApp Routerがなぜ速いのか、なぜServer ComponentとClient Componentを使い分けるのかが見えてくるはずです。私は少し見えました笑。
引き続き、学習頑張ります🔥
Discussion
僭越ですが、関連してこんな記事があるので、良かったら参考になさってください!
記事読ませていただきました!!すごく理解が深まりました(泣)
ありがとうございます!!!!
お助けになれたなら幸いです!