執筆

中川 幸哉

有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表:山田祥寛)に所属するテクニカルライター。新潟県上越市出身。会津大学コンピュータ理工学部卒業後、現在は新潟市に在住。ReactやAndroidを軸に、モバイルアプリ開発やWebサイト制作、Webメディア編集部の業務改善や、プログラミング技術記事の執筆等に携わっている。著書に『たった1日で基本が身に付く! Androidアプリ開発超入門』、『基礎から学ぶ React Native入門』

「たった1日で基本が身に付く! Androidアプリ開発超入門」(技術評論社) 「基礎から学ぶ React Native入門」(翔泳社)

監修

山田 祥寛

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に『「独習」シリーズ』、『改訂新版 これからはじめるReact実践入門』、『改訂3版 JavaScript本格入門』、他。

「独習」シリーズ 「改訂新版 これからはじめるReact実践入門」 「改訂3版 JavaScript本格入門」 他著書多数

はじめに:「ローディングがなぜこんなに多いのか」問題

「ページを開くたびにスピナーが何度も回る...…」

SPA(シングルページアプリケーション)を使っていると、こうした体験に出くわすことがあります。JavaScriptの実行が完了してから初めてデータフェッチが始まり、その結果が揃ってようやくUIが描画される——この逐次処理は、構造的な課題です。

前回はTesting TrophyとStorybookを使ったコンポーネントテストの実践を解説しました。今回は連載の締めに向かう前に、近年のフロントエンド開発で再注目されている「SSRファースト」な設計思想を取り上げます。この思想は、コンポーネントの責務分担に新しい視点をもたらしています。

SPA偏重がもたらした問題

2015年前後からReactやVueが普及し、フロントエンドは「すべてをクライアントで」という方向に大きく振れました。バックエンドはAPIを提供するだけ、UIの構築はすべてクライアントサイドのJavaScriptが担う——この分業は開発体験を向上させましたが、副作用も生みました。

最大の問題がネットワークウォーターフォールです。ページを開くと、まずJavaScriptバンドルのダウンロードと実行が完了するまで何も表示されません。その後にAPIリクエストが始まり、データが返ってきてようやくUIが描画されます。コンポーネントがネストされているとこの連鎖が繰り返され、ユーザーは何度もローディングを待つことになります。

▲SPAのネットワークウォーターフォール——コンポーネントのネストが深いほどローディングが連鎖する

さらに、APIを経由しなければサーバーのリソースにアクセスできないため、本来サーバーで完結できる処理もクライアント-サーバー間の往復が必要になります。シンプルなデータ表示のためだけに、APIの設計・実装・セキュリティ対応が必要になるという本末転倒な状況です。

「PHP的な世界観」の復権

実はネットワークウォーターフォール問題は、かつて「解決済み」でした。PHPやRuby on RailsによるSSR(サーバーサイドレンダリング)はSPAより以前から存在しており、「サーバー側でデータを取得してHTMLを生成してブラウザに返す」というシンプルな仕組みが当たり前でした。

SPAの登場で一度はクライアントに振り切ったフロントエンドが、今、サーバーサイドレンダリングをデフォルトにする考えという「PHP的な世界観」を再評価しています。これがSSRファーストという設計思想です。

SSRファーストの要点はシンプルです。データはサーバーで取得してHTMLに埋め込み、完成した状態でブラウザに届けます。ブラウザはすぐに意味のあるコンテンツを受け取れるため、初期表示が速い。そこにhydration*1を組み合わせることで、Reactコンポーネントのインタラクティビティも維持できます。リアルタイム性よりも表示速度が重要な業務アプリケーションには、特に相性の良いアーキテクチャです。

この設計は、Googleが提唱するCore Web Vitalsとも深く関係します。最初の主要コンテンツが表示されるまでの時間を測るLCP(Largest Contentful Paint)は、完成したHTMLを届けるSSRによって改善しやすい指標です。ユーザー操作への応答性を測るINP(Interaction to Next Paint)は、hydration後に動作するクライアントコンポーネントを最小限に抑えることで対処できます。

▲SSRファーストのリクエストフロー——ブラウザに届く時点でHTMLは完成している

RSCというパイオニア

SSRファーストを推し進める上で画期的だったのが、React Server Components(RSC)です。従来のSSRはページ単位でサーバー/クライアントを選択していましたが、RSCはコンポーネント単位でその選択ができるようになりました。

// サーバーコンポーネントでのデータフェッチ(RSC)
async function UserProfile({ userId }: { userId: string }) {
  // (1) サーバー側でDBに直接アクセスできる(APIルート不要)
  const user = await db.users.findById(userId);


  return (
    <div>
      <h1>{user.name}</h1>
      {/* (2) インタラクションが必要な部分だけクライアントコンポーネントにする */}
      <LikeButton userId={userId} initialCount={user.likeCount} />
    </div>
  );
}

(1)のように、サーバーコンポーネント内ではDBに直接アクセスできます。APIルートを経由しないため、シンプルかつセキュアです。(2)のように、インタラクティビティが必要な部分だけをuse clientのクライアントコンポーネントにする——このコンポーネント単位の境界設計がRSCの革新的な点でした。この思想はIslandアーキテクチャとも重なります。Islandアーキテクチャとは、ページの大部分を静的HTMLとして扱い、インタラクティブな「島(Island)」の部分だけにJavaScriptを送り込む設計パターンです。RSCはこの考え方をReactコンポーネントモデルに落とし込んだものと捉えることができます。

ただし現在、RSCはNext.js App Routerと密接に結びついており、他環境での採用は容易ではありません。学習コストや既存プロジェクトからの移行の難しさも継続的に指摘されています。RSCは「SSRファーストという設計思想を広めたパイオニア」として位置づけ、あくまで選択肢の一つとして捉えるのが現実的です。

広がるSSRファーストのエコシステム

RSCが切り開いた設計思想は、他のフレームワークにも広がっています。その代表格がHonohono/jsxです。

// Hono/jsxでのSSRコンポーネント
import { Hono } from 'hono';
import { UserProfile } from './components/UserProfile';

const app = new Hono();

// (1) ルートハンドラでサーバーサイドレンダリング
app.get('/users/:id', async (c) => {
  const userId = c.req.param('id');
  return c.html(<UserProfile userId={userId} />);
});
// components/UserProfile.tsx
// (2) 非同期コンポーネントとしてサーバーサイドのロジックを記述
export async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId);
  return (
    <div>
      <h1>{user.name}</h1>
    </div>
  );
}

HonoはNode.js・Deno・Bun・Cloudflare Workersなど複数のランタイムで動作する軽量なWebフレームワークです。(1)のようにルーターとJSXを自然に組み合わせられ、(2)のようにコンポーネント内でサーバーサイドのデータ取得を直接記述できます。RSCのような複雑なメンタルモデルを必要とせず、既存のExpressライクな開発体験に近い感覚でSSRファーストの恩恵を得やすい点が特徴です。また、Hono/jsxはhydrationを前提としないため、クライアントにJavaScriptバンドルを送らないシンプルなHTMLレスポンスも容易に実現できます。

RSCとHono/jsxはアプローチは異なりますが、「サーバー側でデータを取得しHTMLとして届ける」という根本思想は共通しています。SSRファーストという設計思想が、特定のフレームワークを超えてエコシステム全体に広がっていることがわかります。

SSRファーストの採用指針

SSRファーストが特に有効なのは、初期表示速度が重要な業務アプリケーション、SEOが必要なコンテンツサイト、認証後のダッシュボードなどです。データをサーバーで取得済みの状態でHTMLを届けられるため、スピナーの連鎖が生まれにくく、ユーザーにはすぐに意味のある画面が表示されます。

既存のSPAプロジェクトに対しては、段階的な移行も選択肢になります。パフォーマンスに課題のあるページから優先的にSSRファーストへ切り替え、効果を確認しながら範囲を広げていくアプローチが現実的です。

SSRとSPAの使い分け

ここまでSSRファーストの利点を中心に解説してきましたが、SPAにも依然として有効な場面があります。WebSocketを使ったリアルタイムコラボレーションツール、カーソル追従やタイムラインのスクラブ操作など複雑なインタラクションが要となるアプリケーション、あるいはElectronのようなデスクトップ文脈では、クライアント中心の設計が自然な選択です。また、すでに動作しているSPAを無理にSSRファーストへ移行することにコストに見合う価値があるかどうかも、現実的に問い直す必要があります。

重要なのは「SSRファーストとSPAのどちらが優れているか」ではなく、「何を優先するか」で判断することです。初期表示速度やSEOが重要なら前者、高い応答性やリアルタイム性が命なら後者——この軸で考えると迷いが減ります。ひとつのアプリケーション内でも、ページや機能の特性に応じて両者を組み合わせる判断も十分あり得ます。

SSRファーストという流れを追い風として活かしつつ、特定のアーキテクチャに縛られない柔軟な判断眼こそが、現代のフロントエンドエンジニアに求められる姿勢です。

SSRファースト時代のコンポーネント3分類

SSRファーストな設計に取り組むと、コンポーネントを「どの環境で動かすか」という新たな軸で考える必要が出てきます。この連載を通じて培ってきたコンポーネント設計の視点から整理すると、3つの分類が浮かび上がります。

ブラウザAPI依存のコンポーネント

windowlocalStorage、ユーザーインタラクションの処理など、ブラウザ固有の機能を使うコンポーネントです。サーバーでは実行できないため、クライアント専用として扱います。インタラクティブな操作を担う性質上、UIの「動き」に関わる処理がここに集まる傾向があります。

サーバーAPI依存のコンポーネント

DBアクセスや環境変数、ファイルシステムなど、サーバー環境のリソースを使うコンポーネントです。セキュリティ上もクライアントには渡せないロジックを含むため、サーバー専用として扱います。機密情報を扱うケースも多く、誤ってクライアント側にロジックが漏れないよう、境界を明示的に意識した設計が求められます。

どちらにも依存しないコンポーネント

受け取ったPropsを表示するだけのコンポーネントや、プラットフォーム非依存の計算ロジックを持つコンポーネントです。サーバーでもクライアントでも動作するため、どちらにでも配置できます。StorybookでのUI確認も容易で、デザインシステムの構成要素として横断的に流用しやすい特徴もあります。

// ✅ サーバーAPI依存:DBへのアクセスを含む
async function UserCard({ userId }: { userId: string }) {
  const user = await db.users.findById(userId);
  return <UserCardView name={user.name} email={user.email} />;
}


// ✅ どちらにも依存しない:Propsを受け取って表示するだけ
function UserCardView({ name, email }: { name: string; email: string }) {
  return <div className="card">{name} - {email}</div>;
}


// ✅ ブラウザAPI依存:クライアントのインタラクションを担う
'use client';
function LikeButton({ userId }: { userId: string }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? '♥' : '♡'}</button>;
}

この3分類で特に重要なのが「どちらにも依存しないコンポーネント」です。前回解説したテスト駆動なコンポーネント開発の観点から見ると、このコンポーネントは最もテストしやすいタイプです。ブラウザ環境のモックもサーバー環境の準備も不要で、Propsの入力と出力の関係だけをシンプルにテストできるからです。

SSRファーストな設計においても、コンポーネントの依存を意識して「どちらにも依存しないコンポーネント」を増やすことが、テスタビリティと再利用性を高める実践的な指針となります。

まとめ

今回は、SSRファーストな設計思想と、それが現代のコンポーネント設計に与える影響を解説しました。

SPA偏重の時代を経て、フロントエンドは再びサーバーを中心に据える方向へシフトしています。RSCはその先駆けとして「コンポーネント単位のサーバー/クライアント選択」という思想をもたらし、Hono/jsxをはじめとするフレームワークがその思想をそれぞれの形で実現しています。

そしてSSRファーストな設計においても、コンポーネントの責務を「ブラウザAPI依存・サーバーAPI依存・どちらにも依存しない」の3種に分類する視点が有効です。依存のないコンポーネントを意識して設計することは、第2回から一貫して語ってきた「責務の明確化」という原則の延長線上にあります。

次回は連載の締めくくりとして、これまで学んできたコンポーネント設計の知識を実際のプロジェクトに活かすための実践ガイドをお届けします。

*1:hydration:サーバーサイドでレンダリングされた静的なHTMLに対し、クライアント側でJavaScriptのイベントハンドラを紐付ける処理のこと。