サーバーコンポーネントとクライアントコンポーネントの使い分け【Next.js】
Next.js (13+のApp Router以降) では、コンポーネントをサーバーコンポーネントとクライアントコンポーネントに分けて利用できます。これは従来のサーバーサイドレンダリング(SSR)とクライアントサイドレンダリング(CSR)の利点を組み合わせ、アプリケーションのパフォーマンスとユーザー体験を向上させるための仕組みです。この記事では、両者の基本概念、使い分けの基準などについて触れたいと思います。
基本概念
サーバーコンポーネントとは
サーバーコンポーネントはレンダリングをサーバー側で行うコンポーネントです。ユーザーがページをリクエストすると、まずサーバー上でコンポーネントが描画され、完成したHTMLがクライアント(ブラウザ)に送信されます。ブラウザは受け取ったHTMLをそのまま表示するため、初期表示が高速で、検索エンジンにも内容が伝わりやすいという特徴があります。Next.jsのApp Routerではデフォルトで全てのコンポーネントがサーバーコンポーネントとして扱われ、特別な指定なしにサーバー側レンダリングが行われます。
サーバーコンポーネントの主な役割や特徴は次のとおりです。
-
非インタラクティブなUIのレンダリング:
ユーザーと直接やり取りしない静的コンテンツの表示に適しています。例えばブログ記事やドキュメント、頻繁に更新されないランディングページなど、初期表示時に完全なHTMLがあれば十分なケースです -
データ取得やバックエンド連携:
サーバー上で動作するため、データベースや外部APIからのデータフェッチ、機密情報(APIキーなど)の安全な取り扱いに向いています。コンポーネント内で直接データを取得でき、取得したデータはサーバーでレンダリング済みの状態でクライアントに送られます。 -
クライアントへのJavaScript送信不要:
サーバーコンポーネントはブラウザに対して純粋なHTMLとして送信されるため、対応するコンポーネントのJavaScriptがバンドルに含まれません。その結果、クライアント側のバンドルサイズが小さくなり初期ロードが高速になるという利点があります。これがパフォーマンス向上に大きく寄与します。
しかしサーバーコンポーネントには制約もあります。ブラウザ上では動作しないため、以下のような点に注意が必要です。
-
イベントハンドリングや状態管理が不可:
サーバーコンポーネント内ではonClickやフォームのonSubmitといったユーザーイベントの処理、あるいはuseStateやuseEffectといったReactフックは使用できません。これらはクライアント側でのみ意味を持つ機能のため、サーバーコンポーネントではコード上に書いても動作せず、Next.jsはエラーで警告してくれます。 -
インタラクティブな要素に不向き:
上記の理由から、ボタン操作や入力フォームなどユーザーと対話するUI部分を直接サーバーコンポーネントとして実装することはできません。インタラクションが必要な場合はクライアントコンポーネントを用いる必要があります。
サーバー環境への依存:
サーバーコンポーネントはNode.js等のサーバーランタイム上で実行されます。そのため、この仕組みを使うにはNext.jsをサーバーサイドで動かす(またはサーバーレス環境でデプロイする)必要があり、クライアントだけでは完結しません。静的ホスティングのみではサーバーコンポーネントの動的機能は活かせない点に留意しましょう。
クライアントコンポーネントとは
クライアントコンポーネントはブラウザ上で実行されるコンポーネントです。サーバーでプリレンダリングされた後、ブラウザ側でJavaScriptによってハイドレーション(再構築)され、インタラクティブに動作します。Next.jsではファイルの先頭に特別なディレクティブ"use client"と記述することで、そのファイルがクライアントコンポーネントとして扱われます。"use client"と宣言されたコンポーネントは、ビルド時に別途クライアント用のバンドルに含まれ、ブラウザ上で実行されるようになります。
クライアントコンポーネントの特徴と用途は次のとおりです。
-
ユーザーとのインタラクション:
クライアントコンポーネントはユーザーの操作に反応するUIを実現できます。クリックや入力、マウス操作などのイベントハンドラを持ち、useStateやuseEffectなどのReactフックを利用して動的に状態を管理・更新できます。例えば、フォーム入力やボタンのクリックカウント、モーダルの表示切替など、リアルタイムでUIが変化する機能はクライアントコンポーネントで実装します。 -
リッチなユーザー体験:
アニメーションやドラッグ&ドロップ、グラフ描画などリッチなUIはクライアント側での操作が不可欠です。クライアントコンポーネントではブラウザの各種API(windowやlocalStorage、あるいはCanvasやWebGLといった描画API)を利用可能なので、リッチで複雑なフロントエンドロジックを実装できます。オフライン対応やページ遷移なしで完結するSPA的な機能も、クライアントコンポーネントによって実現されます。
クライアントコンポーネントにもトレードオフがあります。主なデメリットとして以下が挙げられます。
-
初期ロードの負荷増大:
クライアントコンポーネントはブラウザで動作するため、対応するJavaScriptコードがユーザーの端末に送られ実行されます。その分バンドルサイズが大きくなり、初回表示までのロード時間やハイドレーションによる遅延が発生します。特にコンポーネント数が多いと、ダウンロードとパースに時間がかかり、ユーザーが操作可能になるまでの時間が伸びる傾向があります。 -
状態管理の複雑化:
大規模アプリになると、複数のクライアントコンポーネント間で状態を同期するのは難しくなる場合があります。親子間でデータをやり取りしたり、グローバルな状態管理ライブラリを導入したりする必要が出てきて、サーバーコンポーネント主体の構成と比べてアプリケーション構造が複雑化することがあります。 -
SEOや初期表示の課題:
クライアントコンポーネントのみで構成されたページは、初期表示時にコンテンツが空(ロード中の状態)になりがちです。サーバーからプレレンダリングされたHTMLが十分用意されていないと、検索エンジンがコンテンツを正しく取得できなかったり、ユーザーに白紙のページを長く見せてしまったりするリスクがあります。Next.jsでは必要に応じてサーバーコンポーネントと組み合わせることでこの問題を緩和できますが、純粋なCSRに近い部分が増えるほどSEO対策には工夫が要ります。
レンダリング
このような特徴からユーザーのリクエストからページ生成の流れも異なってきます。
サーバーコンポーネントではサーバーでのデータ取得とレンダリングを行なって、HTMLを生成してから、クライアントへ送信されます。
一方、クライアントコンポーネントはサーバーでプレレンダリングしたのちにHTMLとJSのバンドル生成をしてからクライアント側の処理へと移っていきます。その後、静的HTMLにJSが読み込まれてインタラクティブな状態になります。
2つのコンポーネントを組み合わせたハイブリッドレンダリングの場合、サーバー上でコンポーネントツリーの処理の際にそれぞれのコンポーネントに合わせた分岐処理を行います。サーバーコンポーネント部分はHTMLに変換され、クライアントコンポーネントの位置にプレースホルダーを配置します。その後、それらのバンドルを生成します。バンドルがクライアント側へ送信されると、サーバーコンポーネント側は静的HTMLのためすgに表示され、クライアントコンポーネント側はプレースホルダー部分がインタラクティブなコンポーネントにハイドレーションされて、完全なページとなります。
使い分けの基準
ではどのような場合にサーバーコンポーネントを使い、どのような場合にクライアントコンポーネントを使うべきなのでしょうか。基本的な判断基準としては「そのコンポーネントがユーザーとどれだけ対話するか」と「表示するデータの性質」に着目します。以下に代表的なケースを示します。
サーバーコンポーネントを選ぶべき場合
-
静的または読み取り専用のコンテンツ:
ユーザーからの入力や操作を必要としないページはサーバーコンポーネントが適しています。例えば企業サイトのランディングページ、記事や商品一覧、ドキュメントページなどは、サーバー側で完全にHTMLを生成して高速に表示する方がユーザー体験が良く、SEO的にも有利です。 -
SEOが重要なページ:
公開ブログの記事ページや商品詳細ページなど、検索エンジンでのインデックスやソーシャルメディアでのシェア時に内容が表示されることが重要なページでは、サーバーコンポーネントによるSSRが推奨されます。サーバー側でコンテンツをレンダリングしておけば、クローラは完全なHTML内容を取得できます。 -
初期表示で大量のデータを扱うページ:
ダッシュボードや分析ページのように、一度に多くのデータを表示する必要がある場合、クライアントにそのデータをすべて任せると負荷が高くなります。サーバーコンポーネントでデータを取得・整形し、HTMLとして出力することでクライアントに送るデータ量を削減できます。 -
バックエンドリソースへの直接アクセスが必要な場合:
データベースクエリやサードパーティAPI呼び出しなど、サーバー側でしか実行できない処理があるコンポーネントはサーバー側で動かす必要があります。例えば、サーバーコンポーネント内で直接データベースからデータを取得し、それを表示コンポーネントに組み込むことが可能です。機密情報(APIキーやトークンなど)を扱うロジックもクライアントに露出しないサーバーコンポーネント内で完結できます。
クライアントコンポーネントを選ぶべき場合
-
ユーザー操作に応答する必要がある場合:
ボタンのクリック、フォーム入力、メニューの開閉などユーザーとの対話が発生するUIはクライアントコンポーネントで実装します。例えばお問い合わせフォームやログインフォーム、設定変更画面のように入力を伴う部分は、入力内容のバリデーション表示や送信ボタンの有効/無効切り替えなど動的な処理が必要なため、クライアント側での制御が不可欠です。 -
頻繁にコンテンツが変化・更新される場合:
リアルタイムデータを表示するチャット、通知一覧、ストリーミングのコメント表示、株価や天気のライブ更新など、秒刻み・分刻みで内容が変わるコンポーネントはクライアントコンポーネントが適しています。サーバーで毎回再レンダリングしていては遅延が大きくなるため、WebSocketや定期的なfetchを使ってクライアント側で更新する方が現実的です。 -
高度なUI機能:
インタラクティブなマップ、ドラッグ操作が可能なカンバンボード、グラフライブラリを用いたチャート描画、ゲームのような複雑なUIロジックなど、ブラウザ上で完結する処理が多い場合はクライアントコンポーネントを用います。これらは描画後のユーザー操作が主眼となるため、サーバーで静的に出力するだけでは不十分です。
ハイブリッドなアプローチ
一つのページ内でサーバーコンポーネントとクライアントコンポーネントを組み合わせて使うのが一般的だと思います。例えば、商品一覧ページではリスト表示部分をサーバーコンポーネントでSSRしつつ、各商品に用意した「カートに追加」ボタンをクライアントコンポーネントとして実装するといった具合です。こうすることで、ページ全体の高速表示と部分的なインタラクティブ性の両立が可能になります。
他にも、ブログ記事ページにコメント投稿フォームをクライアントコンポーネントとして差し込んだり、ダッシュボードの静的なフレーム部分をサーバーレンダリングし各種ウィジェットをクライアントコンポーネントで動かしたりするなど、ユースケースに応じて柔軟に使い分けることが重要です。
以上をまとめると、「ユーザーから見て静的であってほしい部分」はサーバーコンポーネント、「ユーザーと対話する必要がある部分」はクライアントコンポーネントと考えると判断しやすくなります。
ユースケース別の使い分け
データフェッチの最適な方法
データの取得(フェッチ)に関しては、Next.jsでは可能な限りサーバーコンポーネント側で行うのが望ましいです。サーバーコンポーネントはサーバー上で直接データ取得処理が書けるため、データベースクエリや外部API呼び出しをそのまま記述できます。そして、取得したデータはサーバー上でReactコンポーネントに変換され、HTMLに埋め込まれてクライアントに送信されるため、ユーザーのブラウザは追加のデータ取得を待つことなくコンテンツを表示できます。その結果、初回表示が速くなりユーザー体験が向上します。
具体例として、ブログの投稿一覧を表示するページを考えてみます。投稿データをデータベースから取得する場合、サーバーコンポーネント内でクエリを発行して一覧を生成すれば、ユーザーはページを開いた時点で全投稿がレンダリングされた状態をすぐに目にできます。
逆にこれをクライアントコンポーネントで実装した場合、最初に空の状態でページが表示され、その後ブラウザ側でuseEffect等を使ってデータを取得し、コンポーネントの状態を更新する処理が走ります。後者ではネットワーク通信の遅延分だけユーザーはコンテンツを見られない時間が発生し、体感速度が落ちてしまいます。
ただし、すべてのデータ取得をサーバー側に寄せるのが絶対というわけではありません。例えばユーザー操作に応じて追加でデータを取得するケースです。ページ遷移なしにコンテンツを更新する必要がある場合(「もっと見る」ボタンでの追加読み込みや、無限スクロールでの継ぎ足し読み込みなど)は、その特定の操作に対応する部分をクライアントコンポーネントとして実装し、必要なときにfetchを呼ぶ方がスムーズです。初期表示に必要なデータはサーバーコンポーネントで取得し、追加読み込み部分のみクライアント側で処理するといったハイブリッドな実装も可能です。このように初期表示のデータ取得はサーバーコンポーネント、ユーザー操作に伴う追加データ取得はクライアントコンポーネントというように使い分けると、パフォーマンスと動的な挙動のバランスが取れます。
ユーザー入力(フォーム)の場合
フォームの入力や送信はユーザーとのインタラクションの典型例であり、基本的にはクライアントコンポーネントで扱います。例えば、コメント投稿フォームやお問い合わせフォームを考えてみましょう。
これらのフォームでは、ユーザーが入力した内容をリアルタイムで検証してエラーメッセージを表示したり、送信ボタンを有効化/無効化したりといった動的なUI変化が求められます。サーバーコンポーネントでは入力中の動的な変化に対応できないため、フォーム自体はクライアントコンポーネントとして実装し、onChangeやonSubmitイベントをJavaScriptで処理する必要があります。
一方で、フォームの送信後の処理自体(データベースへの書き込みやメール送信など)はサーバーで行われることが多いでしょう。Next.jsではフォームの内容を受け取るAPIルートやアクションを定義してサーバー側で処理できます。最近のNext.js (13.4以降) では、サーバーコンポーネント内でフォーム送信を直接受け取るServer Actions機能も導入されつつありますが、これを用いる場合でもUI部分(入力欄やボタン)はやはりクライアントコンポーネントになります。
ユースケースによっては、ページの大部分はサーバーコンポーネントで静的コンテンツを表示し、フォーム部分だけをクライアントコンポーネントとして埋め込むこともあります。例えば製品紹介ページに問い合わせフォームを組み込む場合、紹介文や画像はサーバーコンポーネントでSSRしつつ、ページ下部の問い合わせフォームUIだけクライアントコンポーネントにすることが可能です。このようにすれば、ページ表示の速さとフォームのインタラクティブ性の両方を満たすことができます。
インタラクティブなUIの実装
ユーザーと対話するインタラクティブなUI要素全般に関しては、原則としてクライアントコンポーネントで実装します。例えば以下のようなケースです。
-
ボタンやメニューの操作:
「いいね!」ボタンやドロップダウンメニュー、モーダルダイアログの開閉など、ユーザーの操作に応じてその場でUIが変化する要素はすべてクライアントコンポーネントが担います。サーバーコンポーネントではクリックごとに再レンダリングすることはできないため、これらはブラウザ上のイベントハンドラで即座に反応させる必要があります。 -
ダッシュボードなどの動的ウィジェット:
リアルタイム更新が必要なダッシュボードのウィジェット(グラフ、統計カウンタなど)はクライアント側で定期更新したりPush配信を受けたりして描画します。親となるダッシュボードレイアウトはサーバーコンポーネントで静的部分を描画しつつ、各ウィジェット部分をクライアントコンポーネントで実装することで、初期表示の速さと更新時の滑らかさを両立できます。 -
アニメーションとリッチメディア:
ページ内でアニメーション効果を出したり、音声・動画の再生制御、キャンバス描画をしたりする場合もクライアントコンポーネントが必要です。例えば、スクロールに応じて要素がフェードインするような演出は、window.scrollイベントを用いてクライアント側でDOM操作を行う必要があります。また、地図表示(Google Mapsなどの統合)やカスタムのビデオプレイヤーUIもブラウザAPIを直接扱うためクライアントコンポーネントで構築します。
これらインタラクティブUIは基本的にクライアントコンポーネントですが、それらが載っている土台(レイアウト部分)はサーバーコンポーネントであるという構成が多い点も覚えておきましょう。先ほどの例にもあったように、製品リスト+「カートに追加」ボタンの組み合わせでは、製品リスト表示自体はサーバーコンポーネントで行い、各ボタンのみクライアントコンポーネントにします。このようにページ全体を通してみるとサーバーコンポーネントとクライアントコンポーネントが共存しており、それぞれの得意分野を担当しています。インタラクティブな部分だけをクライアント側に委ね、その周囲をサーバー側で固めるという設計はNext.jsにおけるベストプラクティスの一つです。
パフォーマンスや開発体験への影響
サーバーコンポーネントとクライアントコンポーネントの使い分けは、アプリケーションのパフォーマンスや開発者体験にも大きな影響を与えます。両者がパフォーマンスに及ぼす効果と、開発効率・体験の観点について記述します。
パフォーマンス向上のポイント(サーバーコンポーネント活用)
サーバーコンポーネントを適切に活用することで、Webアプリのパフォーマンスは大きく向上します。主な理由は以下のとおりです。
-
初期表示が速くなる:
サーバーコンポーネントによって予めレンダリングされたHTMLが送られるため、ブラウザは受け取った内容をすぐ表示できます。クライアントコンポーネント中心の構成では、まずJSをダウンロードして実行しないとコンテンツが生成されませんが、サーバーコンポーネントならその待ち時間を削減できます。結果としてファーストビューの表示(First Contentful Paint, FCP)が速くなり、ユーザーは素早くページ内容を確認できます。 -
送信するデータ量の削減:
サーバー側で完結したレンダリング結果(HTML)を送ることで、クライアントに送信するJavaScriptの量を減らせます。特に、非インタラクティブなUIをサーバーコンポーネントにすることで、対応するクライアントJSが不要になりバンドルサイズが小さくなります。これは回線速度が遅いユーザーや低スペックなデバイスでもページを軽快に読み込めることにつながります。 -
サーバーリソースの活用とキャッシング:
重たい計算やデータ集約処理もサーバーコンポーネントで行えば、クライアントの負荷を減らせます。さらに、サーバーでレンダリングした結果をキャッシュしておくことで、同じデータを再利用するページの表示を高速化できます。Next.jsはビルトインでサーバーコンポーネントの結果を再利用(静的生成やISRのような仕組み)できるため、適切にキャッシュ戦略を設定すれば大きなパフォーマンス恩恵があります。 -
SEOとソーシャルシェア:
パフォーマンスとは少し異なりますが、サーバーコンポーネントによるHTML出力はSEOにも寄与します。検索エンジンのクローラはJS実行を待たずにコンテンツを取得できるため評価が高まりやすく、結果的にオーガニックトラフィックの増加につながります。またTwitterやFacebookでURLがシェアされた際にも、埋め込みカードにページ内容(タイトルやサムネイル、概要)が正しく表示されやすくなります。
クライアントコンポーネント利用のトレードオフ
一方で、クライアントコンポーネントを多用しすぎるとパフォーマンス面でいくつかのトレードオフが発生します。
-
初期レンダリングの遅延:
前述のとおり、クライアントコンポーネントは初期表示に必要なJSのダウンロード&実行を伴うため、サーバーコンポーネントに比べ初期描画が遅くなりがちです。特に大規模アプリケーションでクライアントコンポーネントが増えると、その分だけTTI(Time To Interactive)が遅延し、ユーザーがすぐ操作できない状態が長引くことがあります。 -
メモリ・CPU負荷の増加:
ブラウザ上で動作するJavaScriptが増えるほど、ユーザー端末のメモリ消費やCPU使用率も上がります。サーバーコンポーネントであればサーバー側資源(比較的安定した性能のマシン)で行えていた処理を、クライアントコンポーネントでは各ユーザーの環境で処理させることになるため、低性能な端末ではカクつきの原因にもなります。 -
複雑な状態管理:
クライアント側で持つ状態が増えるほど、それらを一貫性のある形で管理するのは難しくなります。状態管理ライブラリ(ReduxやZustand等)を駆使する場面も出てくるでしょう。またサーバーコンポーネントとの境界でデータを受け渡す必要がある場合、Reactのコンテクストやプロパティ経由で橋渡しする実装が必要になるなど、アプリの構造が複雑になります。
クライアントコンポーネントは現代的なリッチUIには不可欠であり、トレードオフはあってもそれ以上の価値を提供してくれる部分でもあります。大事なのは「必要なところだけクライアントコンポーネントにする」ことで、前述したようにNext.jsではサーバーとクライアントを共存させてメリハリを付けた設計が可能です。適材適所で使い分けることで、上記デメリットの影響を最小限に抑えつつ優れたUXを実現できます。
まとめ
Next.jsにおけるサーバーコンポーネントとクライアントコンポーネントの使い分けは、パフォーマンスとインタラクティブ性を両立させる鍵となります。サーバーコンポーネントを活用すれば初期表示の高速化やSEO強化が図れ、一方でクライアントコンポーネントを使えば豊かなユーザー体験を提供できます。それぞれのメリット・デメリットを正しく理解し、適材適所で使い分けることで、Next.jsアプリケーション開発はより効率的かつ高度なものになります。
参考記事
Discussion