意外と知らない React Server Components の真価 — ちゃんとコードを書いて確かめてみた
Server Componentsが「サーバーでHTMLを作ってクライアントに送る仕組み」なのは知ってる。でも、具体的に従来のReactと何がどう変わるのか、コードで見たことはありますか?
自分は「なんとなくサーバーで動くやつ」くらいの理解で使っていた。公式ドキュメントを読んで実際にコードを書いてみたら、バンドルサイズ、データ取得、ローディング体験が具体的にどう変わるのかが見えてきた。
この記事では、従来のやり方とServer Componentsを同じアプリで実装して比較した結果を書く。
Server Componentsの最初の価値:バンドルサイズ削減
ブログ記事のマークダウン変換を考える。従来のReactだと、変換ライブラリ(markedやsanitize-html)をクライアントにまるごとダウンロードさせていた。表示するだけなのに。
"use client";
import { marked } from "marked"; // クライアントにダウンロードされる
import sanitizeHtml from "sanitize-html"; // クライアントにダウンロードされる
export default function PostPage() {
const [post, setPost] = useState(null);
useEffect(() => {
fetch(`/api/posts/${id}`)
.then(res => res.json())
.then(data => setPost(data));
}, []);
const html = sanitizeHtml(marked(post.body));
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
marked(~40kB)とsanitize-html(~50kB)がブラウザに送られる。変換結果を表示するだけなのに、変換エンジンごと送っている。
Server Componentsなら、ライブラリはサーバーでだけ使われ、クライアントには変換済みHTMLだけが届く。
import { marked } from "marked"; // クライアントには送られない
import sanitizeHtml from "sanitize-html"; // クライアントには送られない
export default async function PostPage({ params }) {
const post = await db.posts.get(id);
const html = sanitizeHtml(await marked(post.body));
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
実際にDevTools NetworkタブでJS転送量を比較した。
| JS転送量 | |
|---|---|
従来版("use client" + useEffect) |
839 kB |
| Server Components版 | 703 kB |
| 差 | -136 kB |
136kBの差は、まさにmarkedとsanitize-htmlがクライアントに送られなくなった分。
ちなみに、コンポーネントの判別方法はこう:
- 何も書かない → Server Component(デフォルト)
-
"use client"→ Client Component -
"use server"はServer Componentのマークではない(Server Functionsのマーク。よくある誤解)
Server Componentsの真価:ウォーターフォール解消
マークダウン変換のような静的コンテンツはビルド時に生成できる。でも、IDによって中身が変わる動的なデータ(ブログ記事、ユーザープロフィールなど)はそうはいかない。
ブログ記事の詳細ページを考える。記事データと著者データが必要だが、記事を取得しないと著者のIDがわからない。
ブラウザ → fetch('/api/posts/1') → サーバー → 結果返す
ブラウザ → 記事を表示 → 著者IDがわかった!
ブラウザ → fetch('/api/authors/5') → サーバー → 結果返す
ブラウザ → やっと著者表示
2回のfetchが直列で実行される(=ウォーターフォール)。並列にできない。
useEffect(() => {
fetch(`/api/posts/${id}`) // 1回目
.then(res => res.json())
.then(post => {
setPost(post);
return fetch(`/api/authors/${post.authorId}`); // 2回目(直列!)
})
.then(res => res.json())
.then(author => setAuthor(author));
}, [id]);
Server Componentsなら、サーバー内で全部完結する。
export default async function PostPage({ params }) {
const post = await db.posts.get(id);
const author = await db.authors.get(post.authorId);
// 完成品がクライアントに届く
}
| ブラウザからのfetch | 所要時間 | |
|---|---|---|
| 従来版 | 2回(直列) | 約1.3秒(721ms + 629ms) |
| SC版 | 0回 | サーバー内で完結 |
SC版のNetworkタブには、自分が書いたfetchが1つもない。?_rsc=...というNext.jsの内部リクエストが1回あるだけ。
ReactからDBに直接アクセスできるの?
従来のReactは全部ブラウザで動く。ブラウザからDB接続したら、接続情報がユーザーに丸見え。そもそもブラウザにはDBに接続する仕組みがない。だから間にAPIサーバーを挟む必要があった。
Server Componentsはサーバーで動く。だからDBに直接アクセスできるし、接続情報が漏れる心配もない。APIエンドポイントを自分で作る必要もなくなった。
「全部サーバーで待つと遅くない?」— Suspenseで解決する
さっきのブログ記事ページに、コメント機能を追加したとする。コメントの取得に2秒かかるなら、記事も含めて全部2秒待つのか?
答えは「分けられる」。
-
記事は絶対必要 →
awaitで待つ -
コメントは後でいい →
awaitしないでSuspenseに任せる
export default async function PostPage({ params }) {
const post = await db.posts.get(id); // これは待つ
return (
<div>
<article>{post.title}</article>
{/* コメントは待たない → Suspenseが仮の画面を出す */}
<Suspense fallback={<p>コメントを読み込み中...</p>}>
<Comments postId={post.id} />
</Suspense>
</div>
);
}
Commentsコンポーネント自体がasyncで、内部でawaitする:
export async function Comments({ postId }: { postId: number }) {
const comments = await db.comments.getByPostId(postId);
return (
<ul>
{comments.map(c => <li key={c.id}>{c.body}</li>)}
</ul>
);
}
コメント取得にわざと2秒の遅延を入れて確認した。
- 記事本文・著者情報は即表示される
- コメント部分だけ「コメントを読み込み中...」が出て、2秒後に差し替わる
- ページ全体が真っ白にならない。準備できた部分から見せるストリーミング表示

従来のやり方だと、全データが揃うまで「読み込み中...」になるか、コンポーネントごとにuseEffect + useStateで個別管理するしかなかった。Suspenseなら宣言的に「ここだけ待たせる」が書ける。
ただし、React単体では動かない
ここまでServer Componentsの良さを書いてきたが、大事な前提がある。Server Componentsの仕様はReactが定義したが、React単体では動かせない。
「このファイルはサーバーで実行する」「このファイルはクライアントに送る」という振り分けを、バンドラーがやる必要があるから。React自体はあくまで仕様(USBの規格みたいなもの)で、それを実際に動かすにはNext.jsのようなフレームワークが必要になる。
2026年3月時点の対応状況:
| フレームワーク | RSC対応 |
|---|---|
| Next.js (App Router) | 本番対応済み(唯一) |
| Waku | 実験的対応(Viteベース) |
| Remix | 未対応 |
| 素のReact (Vite等) | 非対応 |
つまり、今のところServer Componentsを本番で使うなら実質Next.js一択。Reactコミュニティでは「ReactがVercel / Next.jsに依存しすぎでは」という声もある。ReactチームのメンバーがVercelに入社している背景もあり、この構造に対する議論は続いている。
まとめ
| ポイント | 従来のやり方 | Server Components |
|---|---|---|
| ライブラリの転送 | クライアントにダウンロード(839kB) | サーバーで使用、完成品だけ送る(703kB) |
| データ取得 | fetch 2回直列(約1.3秒) | サーバー内で完結(fetch 0回) |
| APIエンドポイント | 自分で作る必要あり | 不要(DB直アクセス) |
| ローディング | 全体が「読み込み中...」 | Suspenseで部分表示 |
「サーバーで動くReact」くらいの理解で使っていたが、実際にコードを書いて比較してみると、何がどれだけ変わるのかが数値で見えた。136kBのバンドル削減、fetchが2回から0回、ページ全体のローディングが部分表示に。
「なんとなくServer Components使ってる」から「従来のやり方と何が違うか説明できる」になれたのが、今回一番の収穫だった。
参考:
- React公式ドキュメント — Server Components
- 検証コード: Next.js 16 (App Router) + TypeScript
Discussion