Server Components
React 19 のサーバーで実行されるコンポーネントで、JS バンドルサイズを削減しパフォーマンスを向上させる
Server Components とは
React Server Components (RSC) は、サーバーで実行されるコンポーネントで、JavaScript がクライアントに送信されない。DB アクセスや API キーの使用がサーバーで安全に行え、バンドルサイズが削減される。Next.js App Router ではデフォルトで全コンポーネントが Server Component であり、対話が必要な部品だけを明示的にクライアント側へ切り出す形になる。
設計上の要点は「どちらで動かすか」ではなく「境界をどこに引くか」にある。境界の内側と外側は別の環境で実行されるため、値の渡し方に直列化の制約が生まれ、コードの分割単位もその制約に従うことになる。
Server vs Client Component
2 つの差は実行場所だけではない。クライアントへ JavaScript が送られるかどうかで、使える React の機能も、扱ってよい機密情報も変わる。
| 観点 | Server Component | Client Component |
|---|---|---|
| 実行場所 | サーバー | ブラウザ |
| JS バンドル | 含まれない | 含まれる |
| DB アクセス | 直接できる | API 経由で取得する |
| useState / useEffect | 使えない | 使える |
| イベントハンドラ | 使えない | 使える |
| 宣言 | デフォルト | 'use client' |
使い分け
Server Component はサーバーで実行されるため、DB アクセスやファイル読み取りを直接行える。Client Component はブラウザで実行され、useState や onClick などのインタラクティブな機能を使う。データ取得が主な処理は Server Component、ユーザー操作を伴う処理は Client Component を選ぶ。
// app/users/page.tsx (ディレクティブ無し = Server Component)
import { SearchBox } from './search-box';
async function UserList() {
const users = await db.users.findMany(); // サーバーで直接 DB アクセス
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
export default async function Page() {
return (
<div>
<SearchBox /> {/* 別ファイルの Client Component */}
<UserList /> {/* 同じファイルの Server Component */}
</div>
);
}
// app/users/search-box.tsx
'use client';
import { useState } from 'react';
export function SearchBox() {
const [query, setQuery] = useState('');
return <input value={query} onChange={e => setQuery(e.target.value)} />;
}
対話部品をわざわざ別ファイルにしているのは体裁の問題ではない。'use client' はファイルの先頭に置いて初めて効き、そのファイルと、そこから import した依存も含めてクライアント側のコードになる。1 つのファイルに両方を書き並べて途中でディレクティブを挟む書き方は成立しない。
シリアライズ境界
Server Component から Client Component へ渡す props は、直列化できる値でなければならない。文字列・数値・bigint・真偽値・null・undefined、グローバル登録された Symbol、配列や Map / Set、Date、TypedArray、素のオブジェクト、Promise、Server Function、そして Client / Server Component の要素 (JSX) は渡せる。渡せないのは、クライアント側のモジュールから export されておらず Server Function でもない関数、クラスそのものとクラスのインスタンス、プロトタイプを持たないオブジェクト、グローバル登録していない Symbol である。
この制約は「サーバーでの実行結果だけがネットワークを越える」という設計から出てくる。ORM が返したモデルインスタンスやコールバック関数をそのまま props に載せる設計は境界で破綻するため、渡す直前に必要なフィールドだけの素のオブジェクトへ落とす。落とし穴になりやすいのは、Date だけを見て「そのまま渡せる」と一般化してしまう場合で、同じ日時でも独自クラスで包んでいれば通らない。
境界は逆方向にも効く。Client Component の中で別のコンポーネントを import すると、それがサーバー専用の処理を含んでいてもクライアント側のコードとして扱われる。サーバーでしか動かない子を対話的な親の内側に置きたい場合は、親の中で import するのではなく children として受け取り、組み立てはサーバー側で行う。
バンドルサイズへの影響
送信される JavaScript の内訳が、従来の SSR とは根本的に違う。
従来の SSR:
サーバーで HTML 生成 → クライアントに全 JS を送信 → Hydration
Server Components:
サーバーで HTML 生成 → Client Component の JS のみ送信
Server Component の JS は送信されない → バンドルサイズ削減
Server Actions
Server Actions (React の API 名では Server Function) は、クライアントのコードから呼び出せるサーバー側の関数である。呼び出しは常にネットワーク越しの要求になるため、宣言できるのは async 関数だけで、引数と戻り値は直列化されて往復する。引数として渡せる型は Client Component への props とほぼ同じだが、FormData が加わり、JSX は渡せない。
// app/users/actions.ts (ファイル全体の export を Server Function として公開)
'use server';
export async function createUser(formData: FormData) {
// 引数はクライアント由来 = 信頼できない入力として扱う
const session = await getSession();
if (!session?.canCreateUser) throw new Error('forbidden');
await db.users.create({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
}
// app/users/create-user-form.tsx
'use client';
import { createUser } from './actions';
export function CreateUserForm() {
return (
<form action={createUser}>
<input name="name" />
<input name="email" />
<button type="submit">Create</button>
</form>
);
}
ディレクティブの置き場所には意味がある。'use server' は async 関数の本体の先頭に書いてもよいが、その形の関数はサーバー側のファイルに置いたまま props として渡す使い方に限られる。クライアント側のファイルから import して呼ぶなら、上のようにファイル先頭でモジュール全体に宣言する必要がある。関数の途中や import より後ろに書いても効かず、バックティックの文字列も認識されない。
もう 1 つ、'use server' は「この関数をサーバー用にする」印ではない。既定でサーバー側にあるコードに対して、クライアントから呼べる入口を開ける宣言である。つまり公開エンドポイントが 1 つ増えるのと同じであり、フォームの画面を出さずに直接叩かれる前提で認可と入力検証をこの関数の中に置く。呼び出し側の画面でボタンを隠しても防御にはならない。
判断基準
迷ったときは、その部品が状態やイベントを必要とするかで切り分ける。Server Component は一度描画されたら再描画されず状態も持たないため、状態が要るなら境界の外側 (クライアント側) に置くしかない。
| コンポーネント | 推奨 |
|---|---|
| データ取得、DB アクセス | Server Component |
| 静的な表示 | Server Component |
| フォーム、ボタン、入力 | Client Component |
| useState, useEffect が必要 | Client Component |
| 重いライブラリ (チャート等) | Client Component (lazy load) |
境界の引き方を誤ると、削るはずだったバンドルが逆に増える。ページ最上位に 'use client' を置いてしまうと、そこから import した部品の一切がクライアント側の依存になり、Server Component の利点は消える。境界はできるだけ葉に近い側へ、状態を持つ部品の単位で引く。
この記事は役に立ちましたか?
関連用語
React
コンポーネントベースの UI ライブラリで、宣言的な記法と仮想 DOM の差分計算で UI の更新を扱う
SSR/SSG
サーバーサイドレンダリングと静的サイト生成 - Web ページの生成タイミングと場所を制御するレンダリング戦略
Next.js
React ベースのフルスタックフレームワークで、SSR、SSG、API Routes を統合的に提供する
BFF (Backend for Frontend)
フロントエンドごとに専用のバックエンドを用意し、クライアントに最適化された API を提供するパターン
GraphQL Subscription
GraphQL でサーバーからクライアントへリアルタイムデータ更新を配信する仕組み
リバースプロキシ
クライアントとサーバーの間に位置し、リクエストを中継 / 制御するサーバー
関連する記事
インフラ / クラウド本ガイド - AWS や Docker を本で学ぶ
クラウドインフラ、コンテナ、IaC を学べる技術書の選び方と学習順序を紹介。インフラ本の賞味期限問題と公式ドキュメントとの使い分けも解説します。
AWS 本の選び方 - 全体像 / 構築 / 設計 / 運用 / セキュリティの 5 視点
AWS を学ぶ技術書の選び方を「全体像 / 構築 / 設計 / 運用 / セキュリティ」の 5 視点で整理。公式ドキュメントと本の役割分担、資格対策書の位置づけまで、2026 年 8 月時点の定番書で AWS 独学のルートを解説します。
技術書の中古本 / 古本の賢い買い方 - 安く手に入れて賢く学ぶ
技術書を中古で買うときの判断基準と注意点を解説します。中古で買っていい本と新品で買うべき本の見分け方、状態チェックのポイントを紹介。