React Server Components vs Client-side React (SPA) 比較

サーバーで事前レンダリングされる、ゼロJSがデフォルトのReactレイヤー

VS
Client-side React (SPA)

ブラウザでレンダリングされる、クラシックで成熟したReact利用形態

18 分で読了Frontend

クイック結論

コンテンツ量が多く、SEOが重要な面ではRSCが測定可能な利益をもたらします:react.dev公式の例では75Kのgzipライブラリがclientに一切送られません。高インタラクティブな内部パネル(dashboard、admin)ではclient-firstの方がシンプルでエラー面も少なく済みます。重要な注意点は現実のものです:ServerからClientへオブジェクトのpropをそのまま渡すと、そのオブジェクトの全フィールドがflight payloadに書き込まれます — 境界を誤って引くと利益が静かに失われます。決定は流行ではなく測定で下してください。

React Server ComponentsClient-side React (SPA)
結論をすべて読む

スコア比較

グラフを読み込み中...

詳細スコア

詳細スコア: React Server Components Client-side React (SPA) — カテゴリー別10点満点のスコア
カテゴリーReact Server ComponentsClient-side React (SPA)
パフォーマンス
8/10
6/10
学習のしやすさ
4/10
8/10
エコシステム
7/10
9/10
コミュニティ
7/10
9/10
求人市場
7/10
8/10
将来性
9/10
7/10

長所と短所

React Server Components

長所

  • コンテンツ量の多いページで、重いライブラリ(markdown parser、sanitizer)をclient bundleに一切含めない — react.dev公式の例では75Kのgzip削減
  • サーバー側でのデータ取得により、client-server waterfall(例:Note→Authorの逐次fetch)を排除
  • 初回ロード時にHTMLが即座に表示され、JSのhydrationを待たずにコンテンツを読める
  • Next.js App RouterのISR/cacheレイヤーとネイティブに統合 — stale-while-revalidateで即座に配信
  • React 19.2のActivity + Next.js 16.3のInstant NavigationsによりSPA的な体感の差が大幅に縮まっている
  • Turbopackのchunk-group統合によりclient bundleが自動的に最適化される

短所

  • Server→Client境界を越えるすべてのpropはシリアライズ可能でなければならず、サポートされない値を渡すとReactが例外を投げる
  • 'use client'でファイルをマークすると、そのファイルの全ての推移的importがclient bundleに含まれる — 境界を誤って引くと恩恵が静かに失われる
  • Server Componentはcontextを作成できず、useState/useEffectのようなインタラクティブなAPIを使用できない
  • RSCの内部APIはReactのマイナーバージョン間でsemverに従わない — bundler/framework固有のバージョンにピン留めする必要がある
  • 2025年12月3日に重大な未認証RCE脆弱性が発見された(19.0.1/19.1.2/19.2.1でパッチ済み) — フレームワーク依存のセキュリティ面が拡大している
  • 学習曲線は急 — サーバー/client境界を正しく引くには経験が必要で、エラーメッセージの多くは実行時に現れる

最適な用途

コンテンツ量が多く、SEOが重要なページ(ブログ、製品、ドキュメント、比較ページ)データソースに近く、サーバーで一度に取得できる初回ロード重視の画面ISR/cacheで配信され、頻繁には変わらないがトラフィックの多いページすでにNext.js App Routerに依存し、フレームワークネイティブなアーキテクチャを求めるチームclient JS bundleサイズを測定可能な形で削減する必要があるプロジェクト

Client-side React (SPA)

長所

  • シンプルなメンタルモデル:すべてのcomponentが常にインタラクティブになり得る、サーバー/client境界を考える必要がない
  • Viteベースのtoolingによる高速なdev-server、HMR、成熟したcode-splitting(Rollup)
  • シリアライズ境界がない — stateはブラウザ内で直接JSオブジェクトとして存在し、'flight payload'の罠がない
  • 最大かつ最も歴史あるReactエコシステム — ライブラリ、Stack Overflowの回答、採用プールが最も広い
  • サーバー稼働の必須要件がない — 静的ホスティング/CDNにデプロイ可能
  • デバッグが単一環境(ブラウザDevTools)で完結し、server/clientの二重エラー面がない

短所

  • 初回ロードは通常空の`<div id="root">` + JS bundle — TTIとFCPが近接し、JSが実行されるまでコンテンツが表示されない
  • 重いライブラリ(markdown parser、sanitizer、chart lib)は常にclient bundleに含まれ、サーバー側に残す選択肢がない
  • データ取得が逐次`useEffect`チェーンに傾くと、client-server waterfallのリスクが高い
  • 単一chunk/ページ単位chunk戦略は共有コードを繰り返しダウンロードさせる可能性がある — Turbopackが解決しようとしている問題は、SPA側ではtooling(Vite/Rollup)に委ねられたままになる
  • SEOと初回ロードパフォーマンスのために別途SSG/prerenderレイヤーを構築する必要があり、フレームワーク自体はそれを提供しない

最適な用途

高インタラクティブな内部パネル(admin、dashboard) — 頻繁なstate変更、ドラッグ&ドロップ、リアルタイムフィルタリング認証が必要で、SEOが優先事項ではないアプリケーション小規模チームによる高速なプロトタイピングとMVP開発静的ホスティング/CDNへのデプロイを想定し、サーバー稼働コストを避けたいプロジェクト既存の大規模SPAコードベースの保守と段階的なモダナイゼーション

コード比較

React Server Components
// Next.js App Router — Server Component(デフォルト)
// app/posts/[slug]/page.tsx
import { db } from '@/lib/db';
import LikeButton from './like-button'; // Client Component(別ファイルで 'use client')

interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function PostPage({ params }: PageProps) {
  const { slug } = await params;

  // サーバー側で直接await — client-server waterfallなし、
  // marked/sanitize-htmlのような重いライブラリはclient bundleに一切含まれない
  const post = await db.post.findUnique({ where: { slug } });
  if (!post) return <div>見つかりません</div>;

  return (
    <article>
      <h1>{post.title}</h1>
      {/* シリアライズ可能なpropのみがClient Componentに渡される */}
      <div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
      <LikeButton postId={post.id} initialCount={post.likeCount} />
    </article>
  );
}

// app/posts/[slug]/like-button.tsx
'use client';
import { useState, useTransition } from 'react';

export default function LikeButton({ postId, initialCount }: { postId: string; initialCount: number }) {
  const [count, setCount] = useState(initialCount);
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(async () => {
        setCount((c) => c + 1);
        await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
      })}
    >
      いいね ({count})
    </button>
  );
}
Client-side React (SPA)
// Vite + React — Client-side SPA
// src/pages/PostPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';

interface Post {
  id: string;
  title: string;
  renderedHtml: string;
  likeCount: number;
}

export default function PostPage() {
  const { slug } = useParams();
  const [post, setPost] = useState<Post | null>(null);
  const [isLiking, setIsLiking] = useState(false);

  useEffect(() => {
    // すべてのデータ取得はclient側 — waterfallリスクはここから始まる
    let cancelled = false;
    fetch(`/api/posts/${slug}`)
      .then((res) => res.json())
      .then((data) => { if (!cancelled) setPost(data); });
    return () => { cancelled = true; };
  }, [slug]);

  if (!post) return <div>読み込み中...</div>; // JS実行前にこの画面は一切表示されない

  async function handleLike() {
    setIsLiking(true);
    setPost((p) => (p ? { ...p, likeCount: p.likeCount + 1 } : p));
    await fetch(`/api/posts/${post!.id}/like`, { method: 'POST' });
    setIsLiking(false);
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
      <button disabled={isLiking} onClick={handleLike}>
        いいね ({post.likeCount})
      </button>
    </article>
  );
}

// vite.config.ts — code-splittingはRollupに委ねられる
export default {
  build: {
    rollupOptions: {
      output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router'] } },
    },
  },
};

結論

コンテンツ量が多く、SEOが重要な面ではRSCが測定可能な利益をもたらします:react.dev公式の例では75Kのgzipライブラリがclientに一切送られません。高インタラクティブな内部パネル(dashboard、admin)ではclient-firstの方がシンプルでエラー面も少なく済みます。重要な注意点は現実のものです:ServerからClientへオブジェクトのpropをそのまま渡すと、そのオブジェクトの全フィールドがflight payloadに書き込まれます — 境界を誤って引くと利益が静かに失われます。決定は流行ではなく測定で下してください。

無料相談を受ける
FAQ

よくある質問

コンテンツ量が多く、SEOが重要で、初回ロード(LCP/TTI)が重要な面(ブログ、製品ページ、比較ページ)では、RSCが測定可能な利益をもたらします — サーバー側でのデータ取得とclient JSの削減です。Next.js 16.3自身のデータによると、サーバー側レンダリングでNode.jsネイティブstreamへの移行により、負荷時に22%多くのリクエストを処理できます。高インタラクティブな内部パネル(dashboard、admin)では、client-firstの方が一般的にシンプルな開発体験を提供します。

関連ブログ記事

すべての記事を見る
すべての比較