フロントエンド・API連携

React Server Componentsのデータ転送:RSCペイロードの構造とストリーミングの仕組み

seoTitle: RSCペイロードの構造とストリーミング…

React Server Componentsのデータ転送:RSCペイロードの構造とストリーミングの仕組み

metaDescription: React Server Componentsのデータ転送の仕組みを、RSCペイロードの構造、Suspense、クライアント側の復元まで詳しく解説

slug: react-server-componentsnodetazhuan-song

React Server Componentsのデータ転送:RSCペイロードの構造とストリーミングの仕組み

React Server Componentsを使い始めると、サーバーからブラウザへ何が送られているのか分かりにくい場面に出会います。Next.jsのApp Routerでサーバーコンポーネントを表示すると、ブラウザのデベロッパーツールには、通常のHTMLとも、APIが返す単純なJSONとも違うデータが流れます。

その正体が、RSC Payload、またはFlight Payloadと呼ばれるデータです。

RSC Payloadは、サーバーコンポーネントの実行結果、クライアントコンポーネントの参照、Props、Suspense境界の状態などを、クライアント側でReactのUIツリーに復元できる形へ変換したものです。完成済みのHTMLをそのまま返すのではなく、Reactがツリーを組み立て直すための情報を、必要な順番でストリームに載せて送ります。

ここを理解すると、サーバーコンポーネントとクライアントコンポーネントの境界、Suspenseによる段階的な表示、Propsのシリアライズ制約が一つの仕組みとしてつながります。「なぜHTMLだけでは足りないのか」「なぜサーバーコンポーネントのコードがブラウザに送られないのか」といった疑問も、RSC Payloadの構造から説明できます。

RSCペイロード:HTMLでもJSONでもない特殊なシリアライズ形式

まず押さえたいのは、RSC Payloadを通常のJSONレスポンスと同じものとして扱わないことです。

一般的なAPIは、オブジェクトをJSONへ変換してレスポンスとして返します。受け取った側はJSON全体を読み込み、JSON.parseでオブジェクトへ戻します。一方、RSC Payloadは、Reactの要素ツリーやモジュール参照を、React専用のルールに従って表現するデータです。

実装やフレームワークの設定によって転送形式の細部は異なりますが、テキストとして送られる場合は、複数のレコードを区切って処理できる行指向の形式がよく使われます。各レコードには識別子や種別が付いており、クライアントはそれを手掛かりにして、ツリーの一部、モジュール参照、遅延中の値などを解決していきます。

したがって、RSC Payloadは次のように考えると分かりやすくなります。

  • HTMLのように、ブラウザがそのまま画面へ表示する完成済み文書ではない
  • APIのJSONのように、アプリケーションが自由に解釈するデータモデルでもない
  • Reactがサーバーで処理したコンポーネントツリーを、クライアントで再構成するための転送表現である
  • すべての結果を一度に待つのではなく、解決した部分から順番に処理できる
  • クライアントコンポーネントの実装コードではなく、そのモジュールを参照する情報を含む

なぜJSONやHTMLだけでは足りないのか

サーバーからデータを送るだけなら、JSONで十分に見えます。しかし、React Server Componentsが扱っているのはデータだけではありません。サーバーコンポーネントの出力には、要素の種類、親子関係、Props、クライアントコンポーネントの配置場所、遅延している境界との関係が含まれます。

単一のJSONオブジェクトにすべてを詰め込む設計にすると、アプリケーション側でコンポーネントツリーを再構築する処理を別途実装しなければなりません。さらに、非同期処理の完了をレスポンス全体の完成条件にすると、ページの一部だけ先に表示することも難しくなります。

HTMLにも別の限界があります。HTMLは、ブラウザが初期画面を描画するには向いています。しかし、そこに含まれる要素がどのクライアントコンポーネントに対応するのか、どのPropsを渡すのか、どのSuspense境界の配下にあるのかを、Reactが再利用できる形で表現するのは簡単ではありません。

SSRで返されるHTMLには、見た目としての結果はあっても、Reactのコンポーネントツリーを再び扱うための情報が十分に残っているとは限らないからです。

RSC Payloadは、この間を埋める役割を持ちます。サーバー側でデータ取得やコンポーネントの評価を行い、その結果をReactの要素ツリーとしてクライアントへ渡す。クライアント側では、そのツリーを読み込みながら、必要なクライアントコンポーネントだけを手元のJavaScriptモジュールと結び付けます。

ただし、RSCを使ったからといって、ブラウザが常に待ち時間なしで操作可能になるわけではありません。表示の早さ、操作可能になるまでの時間、JavaScriptの読み込み、サーバー処理の速度はそれぞれ別の要素です。RSC PayloadはデータとUIツリーの転送を効率化しますが、ネットワークやクライアント側の処理全体を自動的に高速化する魔法ではありません。

RSCペイロードに含まれる情報

RSC Payloadの中身を、役割で分けると次のようになります。

  • サーバーコンポーネントが生成した要素ツリーの表現
  • クライアントコンポーネントのモジュール参照
  • サーバーからクライアントへ渡すProps
  • Promiseなど、後から解決される値への参照
  • Suspense境界のフォールバックと、後続の結果を結び付ける情報
  • すでに送ったデータを再利用するための参照

重要なのは、ここに必ずしも最終的なDOMがそのまま入っているわけではないことです。ReactはPayloadを読み込み、要素の種類とPropsを解決し、必要なクライアントコンポーネントをモジュール参照から見つけ、最終的なUIとして扱います。

項目HTMLによるSSRJSON APIRSC Payload
主な役割ブラウザに表示する文書アプリケーション用のデータReactツリーを復元するための記述
クライアントコンポーネントの参照標準的には持たないアプリ側で設計するモジュール参照として扱う
Suspenseとの関係HTMLのストリーム実装に依存アプリ側で個別に設計Reactの境界と連動する
受信単位HTMLの断片通常はJSON全体レコードやチャンク単位
クライアント側の処理HTMLを解析して表示アプリがデータを解釈Reactがツリーと参照を解決
RSC Payloadは、画面を直接描くHTMLではなく、Reactが画面を組み立てるための中間表現です。

Wire Formatの内部構造:M・J・@プレフィックスが果たす役割

RSC Payloadを調べていると、先頭に特定の文字が付いたレコードを目にすることがあります。解説ではM、J、@といった記号で説明されることがありますが、ここには注意が必要です。

これらはReactの内部実装や、利用しているReactおよびフレームワークのバージョンに依存するWire Formatの一部です。公開APIとして、アプリケーションが直接パースすることを前提にした安定仕様ではありません。学習のために役割を理解するのは有用ですが、特定の接頭辞やレコード構造を自作コードの契約として固定するのは避けたほうが安全です。

Mプレフィックス:クライアントコンポーネントのモジュール参照

Mで始まるレコードは、クライアントコンポーネントを解決するためのモジュール情報を表すものとして説明できます。

たとえば、'use client'を付けたコンポーネントがサーバーコンポーネントから使われている場合、Payloadには、そのコンポーネントのソースコードそのものではなく、クライアント側のモジュールを特定するための情報が含まれます。そこには、モジュールの識別子、エクスポート名、関連するチャンクなどが含まれることがあります。

ここで大切なのは、サーバーコンポーネントがクライアントコンポーネントの実装コードをPayloadへ埋め込んでいるわけではないということです。サーバーは、この場所にはクライアント側のこのモジュールを配置する、という参照を渡します。実際のコードは、クライアント向けにビルドされたJavaScriptのチャンクから読み込まれます。

この分離によって、データベースへのアクセスやサーバー専用の処理を含むサーバーコンポーネントのコードを、クライアントのJavaScriptバンドルへそのまま持ち込まずに済みます。

Jプレフィックス:シリアライズされた要素ツリー

Jで始まるレコードは、サーバー側で評価されたReact要素や、その周辺のデータを表すものとして扱われます。

静的な見出しであれば、要素の種類、Props、子要素の関係を表す情報になります。実際のPayloadでは、単純な文字列だけでなく、別レコードへの参照、遅延中の値、特殊な値を示す記号などが組み合わされることがあります。

ここで表現されるのは、ブラウザがすぐに扱うDOMノードの一覧ではありません。Reactが要素ツリーとして解釈できる形の情報です。サーバーコンポーネントの中にクライアントコンポーネントが含まれる場合も、Jの側にはその配置とPropsが記録され、モジュール自体の情報は別のレコードから解決されます。

同じPayloadの中に、画面に表示する文字列、HTML要素の種類、子要素の配列、クライアントコンポーネントの参照が混在するように見えるのは、そのためです。APIレスポンスのように、すべての値が業務データとして並んでいるわけではありません。

@プレフィックス:レコード間の参照

@で始まる表現は、別のレコードや遅延値を参照するための識別子として現れることがあります。説明上は、モジュール情報と要素ツリーの配置を結び付ける橋渡しと考えると理解しやすいでしょう。

ただし、M、J、@の役割を固定的な三分類として覚えすぎるのは危険です。Wire Formatのレコードは、モジュール参照、モデル、エラー、Promiseの解決結果などを扱うため、実際の出力では複数の参照形式が登場します。また、開発用ビルドと本番ビルド、Reactのバージョン、フレームワーク側の実装によって、見える文字列や番号が変わる場合もあります。

開発者が知っておくべきなのは、特定の記号を暗記することではありません。次の三つの関係です。

1. クライアントコンポーネントは、サーバーから実装コードとして送られるのではなく、クライアントモジュールへの参照として扱われる

2. サーバーコンポーネントの結果は、React要素ツリーを復元できる形で送られる

3. 複数のレコードやチャンクが、識別子によって相互に参照される

この見方を持っていれば、内部形式が変わっても、Payloadが担っている仕事を見失いにくくなります。

サーバーからクライアントへのストリーミング伝送プロセス

RSCの特徴は、コンポーネントツリー全体の準備が終わるまで、クライアントを待たせる設計だけに限定されないことです。解決できた部分を先に送り、まだ準備できていない部分は後続のチャンクで補います。

サーバー側では、環境やアダプターに応じて、ReadableStreamを返すReactのサーバーDOM向けAPIが使われます。代表的なAPIとしてrenderToReadableStreamが知られていますが、実際にどのAPIが使われるかは、ランタイムやフレームワークの構成によって異なります。

そこで生成されたストリームは、HTTPレスポンスのボディへ接続されます。サーバーがすべてのデータを一つの文字列へ連結してから返すのではなく、レンダリングの進行に応じて読み出せる状態を保つことがポイントです。

サーバー側で起きていること

非同期処理を含まない部分であれば、サーバーは比較的早い段階で要素ツリーを組み立てられます。一方、データ取得を待つコンポーネントに遭遇すると、その部分の解決を保留します。

Suspense境界がある場合、保留された部分の代わりにフォールバックを使って、先に解決できるツリーを出力できます。データ取得が終わった後は、保留されていた境界に対応する結果を新しいチャンクとしてストリームへ追加します。

大まかな流れは次のとおりです。

1. サーバーがルートのコンポーネントツリーを評価する

2. 同期的に解決できる要素やPropsをPayloadへ変換する

3. 非同期処理で待機する部分を、Suspense境界に対応付ける

4. 初期表示に必要なレコードとフォールバックを先に送る

5. 非同期処理が解決した境界の結果を、後続チャンクとして送る

6. クライアントが識別子を使って、後続結果を該当する境界へ結び付ける

この方式では、最初のレスポンスが早く届く可能性があります。ただし、ネットワークのバッファリング、圧縮、プロキシ、CDN、サーバーの実装によっては、細かく生成されたチャンクがクライアントへ同じ粒度で届くとは限りません。ストリーミング対応のコードを書いたからといって、必ず見た目の更新が細かく分割されるわけではない点は、実運用で見落としやすいところです。

クライアント側の受け取り

クライアントでは、RSC Payloadを読み込むためのReact側の仕組みが、HTTPレスポンスやReadableStreamを処理します。低レイヤーの構成では、createFromReadableStreamcreateFromFetchのようなAPIが登場しますが、Next.jsのようなフレームワークを使っている場合、通常は開発者が直接呼び出す必要はありません。

クライアント側では、受信したレコードを順番に処理しながら、次の仕事を進めます。

  • 受信した要素ツリーの情報を内部モデルへ登録する
  • 参照IDから別のレコードやモジュール情報を解決する
  • Suspense境界に対応する保留状態を更新する
  • クライアントコンポーネントへPropsを渡す
  • Reactの更新処理を通して、必要な箇所をDOMへ反映する

つまり、RSC Payloadの受信とDOM更新は、単純な文字列置換ではありません。Reactの内部では、ツリーの参照関係と境界の状態を保ちながら、到着した情報を段階的にUIへ統合しています。

Suspense境界と非同期コンポーネントの逐次レンダリング

Suspenseは、RSCストリーミングを理解するうえで中心となる仕組みです。非同期コンポーネントをどこで区切るかによって、最初に表示できる内容と、後から更新される範囲が決まります。

たとえば、ページ全体に一つだけSuspense境界を置くと、境界の中にある処理が終わるまで、その範囲は一つのフォールバックとして扱われます。反対に、一覧、詳細情報、関連コンテンツなどを別々の境界で囲めば、それぞれの準備ができたタイミングで個別に置き換えられます。

ただし、境界を増やせばよいわけではありません。細かく分割しすぎると、フォールバックの設計が複雑になり、更新の順番が利用者にとって不自然になることがあります。チャンクや境界の管理に伴う処理も増えるため、コンポーネントの責務と画面上の意味を見ながら配置する必要があります。

フォールバックが先に表示される仕組み

Suspense境界の内側で非同期処理が保留されている場合、サーバーは境界の外側やフォールバックを含む、先に解決できる部分をPayloadへ載せます。クライアントはその情報を受け取り、ページのレイアウトや読み込み中の表示を組み立てます。

その後、データ取得とサーバー側のレンダリングが完了すると、対応する境界の結果が追加で送られます。クライアントはそのチャンクを既存の参照先と結び付け、フォールバックを実際の内容へ更新します。

ここで注意したいのが、フォールバックが表示された時点で、その部分が必ず操作可能になるわけではないことです。フォールバックが単なるテキストやスケルトンであれば、そもそも操作対象ではありません。ボタンのような見た目を持っていたとしても、クライアントコンポーネントのJavaScriptやイベント処理が準備できていなければ、期待どおりに操作できない場合があります。

RSCとSuspenseによって初期表示が早く感じられるケースはありますが、それをそのままTTIの改善と断定することはできません。TTIは、ページ内の主要な操作が実際に動作する状態になるまでの時間であり、次のような条件に左右されます。

  • クライアントコンポーネントのJavaScriptバンドルのサイズ
  • モジュールのダウンロードと評価にかかる時間
  • ブラウザのCPU負荷
  • イベントハンドラーの登録状況
  • ネットワークやキャッシュの状態
  • Suspense境界の内側にどの操作要素を置いているか

RSCが担当するのは、主にサーバー側でのコンポーネント処理と、その結果の転送です。クライアント側の操作性まで自動的に保証するものではありません。

フォールバックが早く見えることと、ページ全体が早く操作できることは同じではありません。表示、データ到着、JavaScriptの準備を分けて観察する必要があります。

境界の粒度をどう考えるか

Suspense境界は、通信の都合だけでなく、画面の意味で区切るのが扱いやすい方法です。

  • ページ全体の骨格やナビゲーションは、データ取得に巻き込まない
  • 取得に時間がかかる一覧は、一覧として一つの境界にする
  • 独立して読み込めるサイドバーや関連情報は、必要に応じて分離する
  • ユーザーが一体として認識する情報を、細かく分断しすぎない
  • 操作に必要なクライアントコンポーネントは、フォールバック表示だけで利用可能だと誤解しない

この設計を行うと、ストリーミングのメリットと、画面の分かりやすさを両立しやすくなります。

クライアント側でのUIツリー復元とモジュール参照の解決

RSC Payloadを受け取ったクライアントは、サーバーコンポーネントのソースコードを実行して画面を再現するわけではありません。サーバーが生成した要素ツリーを読み込み、そこにクライアント側のモジュールを接続します。

この違いは、通常のSSR後のハイドレーションと混同されがちです。クライアントコンポーネントはブラウザ側でJavaScriptを実行し、イベント処理などを使える状態にする必要があります。一方、サーバーコンポーネントそのものはブラウザへ実装コードを送って再実行する対象ではありません。

段階的な復元プロセス

クライアント側の復元を概念的に分けると、次のようになります。

1. HTTPレスポンスからRSC Payloadの最初のレコードを読み込む

2. ルート要素や、すぐに解決できる子要素を内部のReactモデルへ登録する

3. Suspense境界の外側と、境界内のフォールバックを表示する

4. クライアントコンポーネントのモジュール参照を解決する

5. 参照されたコンポーネントへシリアライズ済みのPropsを渡す

6. 後続チャンクが届いたら、該当する境界や参照を更新する

7. Reactのレンダリング処理を通して、必要なDOMだけを変更する

ここで、クライアントコンポーネントの参照を解決するためには、クライアント向けバンドルに対応するモジュールが存在していなければなりません。サーバーから送られた識別子と、ビルド時に生成されたモジュールマップが対応することで、Reactは該当するコンポーネントを見つけられます。

Server Componentはハイドレーション対象外

サーバーコンポーネントは、クライアントコンポーネントと同じ意味でハイドレーションされません。サーバーコンポーネントのコードがブラウザへ送られ、ブラウザ上で同じデータベース処理をもう一度実行するわけではないからです。

その結果、次のような構成が可能になります。

  • データベースやファイルシステムへのアクセスをサーバー側に閉じ込める
  • サーバーコンポーネントの実装をクライアント用バンドルから分離する
  • 操作が必要な部分だけをクライアントコンポーネントにする
  • 静的な表示やデータ取得の多くをサーバー側で処理する

ただし、サーバーコンポーネントからクライアントコンポーネントへPropsを渡す境界では、値を転送できる形にする必要があります。ここでいうシリアライズ可能性は、単純なJSON互換性と完全に同じではありません。ReactのServer Components向けシリアライザーが、どの値を扱えるかを確認することが重要です。

RSCにおけるDate、Map、Set、正規表現の扱い

Propsのシリアライズについて、古い説明では「JSONに変換できる値だけを渡せる」とまとめられることがあります。しかし、RSCのデータ転送で実際に確認すべきなのは、JSON.stringifyが扱えるかどうかだけではありません。

ReactのServer Components向けシリアライザーは、通常のJSONより広い種類の値を扱える場合があります。とくにDate、Map、Setについては、利用しているReactやフレームワークのバージョン、データの中身、境界の条件を確認せずに、すべて不可と断定するのは正確ではありません。

Dateは直接渡せる場合がある

Dateオブジェクトについては、RSCのシリアライザーが対応している構成であれば、サーバーコンポーネントからクライアントコンポーネントへ直接渡せます。従来のJSONシリアライズだけを前提にすると、Dateは文字列化されるものとして扱われますが、RSCではReact側の復元規則が別に存在します。

そのため、DateをPropsへ入れたら必ず失敗する、という説明は適切ではありません。

ただし、直接渡せることと、直接渡すのが常に最善であることは別です。表示用の日時なら、タイムゾーンやロケールをどこで決めるのかを明確にする必要があります。サーバーとブラウザで表示環境が異なると、同じ時刻でも見た目の文字列が変わる可能性があります。

実務では、次のように使い分けると判断しやすくなります。

  • クライアント側でDateとして扱う必要があるなら、RSCが対応する形で直接渡す
  • APIや永続化層との境界では、ISO形式などの文字列に変換して扱う
  • 画面へ表示するだけなら、サーバー側で表示用の文字列へ整形する
  • 日時の解釈が重要な処理では、タイムゾーンと形式をPropsの設計に含める

アプリケーションの構成によっては、明示的に文字列へ変換したほうが、データの意味を追いやすくなる場合もあります。しかし、それはRSCではDateが一律に送れないからではなく、境界を単純に保つための設計上の選択です。

MapとSetも一律にJSON互換へ変換する必要はない

MapやSetも、RSCが対応する範囲ではシリアライズ可能な値として扱われます。Mapはキーと値の組み合わせ、Setは要素の集合として復元されるため、単純にJSONオブジェクトへ変換しなければならないとは限りません。

ただし、Mapのキーや値、Setの要素が何でも送れるわけではありません。内部に関数、循環参照、未対応のクラスインスタンスなどが含まれていれば、別の問題になります。また、フレームワークやReactのバージョンによってサポート範囲が変わる可能性もあるため、実際のプロジェクトでは公式ドキュメントとビルド時のエラーを確認するのが安全です。

MapやSetを直接Propsに渡すか、配列やプレーンオブジェクトへ変換するかは、次の観点で決められます。

  • クライアント側でもMapやSetのAPIを使う必要があるか
  • データをURL、キャッシュ、ログ、テストで扱う予定があるか
  • シリアライズ後の形をチーム内で明確に共有できるか
  • 要素の順序や重複排除の意味を保つ必要があるか
  • RSC以外のAPI境界でも同じデータを利用するか

たとえば、クライアント側で検索用のMapを使う必要があるなら、対応状況を確認したうえでMapを渡す方法があります。一方、画面表示用の一覧であれば、配列のほうがPropsの形を追いやすく、別の境界へ持ち出す場合にも扱いやすいでしょう。

正規表現は別扱いにする

RegExp、つまり正規表現オブジェクトは、Date、Map、Setと同じように扱えるとは考えないほうがよい値です。正規表現をクライアントコンポーネントへ渡す必要がある場合は、パターンとフラグを文字列などのプリミティブな値に分け、受け取った側でRegExpを生成する設計が現実的です。

この方法なら、どの値をネットワークへ送っているのかが明確になります。さらに、ユーザー入力をパターンとして扱う場合は、正規表現の生成そのものに伴う例外や、意図しない複雑なパターンにも注意できます。

シリアライズ可能性を確認する視点

Server ComponentからClient Componentへ値を渡すときは、「JSONにできるか」だけで判断せず、次のように確認します。

  • ReactのRSCシリアライザーが、その組み込み型をサポートしているか
  • オブジェクトの内部に未対応の値や循環参照がないか
  • その値をクライアントへ送ること自体が妥当か
  • 表示用の文字列や識別子へ変換したほうが境界を明確にできないか
  • Reactやフレームワークのバージョンアップで挙動が変わる余地はないか

関数、一般的なクラスインスタンス、循環参照を含む構造は、特に問題になりやすい領域です。コールバックをそのまま渡すのではなく、Server ActionやAPIなど、フレームワークが用意する別の通信経路を使う必要があります。

RSCのシリアライズ制約は、JSONの可否だけでは決まりません。Reactが復元できる値か、そしてその値をクライアントへ送る設計に意味があるかを分けて考えるのがポイントです。

実際にRSCペイロードを確認する方法

RSC Payloadの実体を確認するには、ブラウザのNetworkパネルを使います。ただし、Chrome DevToolsにRSC専用の「response stream as text」という設定があるわけではありません。そのような項目を探しても見つからないため、実在する機能に沿って確認します。

Chrome DevToolsで見る基本手順

Next.jsなどでRSCを使うページを開き、次の手順でリクエストを調べます。

1. Chromeで対象ページを開く

2. デベロッパーツールを開き、Networkパネルへ移動する

3. 必要であれば、キャッシュを無効化する

4. ページを再読み込みする

5. ページ文書のリクエストや、画面遷移時に発生したFetch・XHRのリクエストを確認する

6. リクエストのHeadersで、RSC関連のリクエストヘッダーやレスポンスヘッダーを確認する

7. Responseタブで本文を確認する

8. Previewタブが構造化表示を試みる場合は、Responseタブと見比べる

9. Timingタブで、リクエスト開始からデータが届くまでの時間を確認する

Next.jsのバージョンや遷移方法によって、RSC Payloadがページ文書のレスポンスに含まれる場合と、クライアント遷移に伴う別リクエストとして現れる場合があります。常に一つの同じ名前のリクエストが表示されるとは限りません。

開発環境では、レスポンス本文に読みにくいレコードが並び、MやJ、参照記号らしき表現を確認できることがあります。ただし、圧縮やバッファリング、開発用の補助情報によって、送信時のチャンク境界がそのままDevToolsの表示へ反映されるとは限りません。

ストリーミングの到着順を確かめる

NetworkパネルのResponse表示は、レスポンス全体を受信した後に確認することがあります。そのため、最初のチャンクと後続チャンクがいつ届いたのかを、Response本文だけで正確に判断できるとは限りません。

到着の過程を見たい場合は、次の情報を組み合わせます。

  • NetworkパネルのWaterfallで、リクエスト開始と完了の時間を見る
  • Timingタブで、待機時間と受信時間を確認する
  • DevToolsのPerformanceパネルで、描画やスクリプト実行のタイミングを見る
  • サーバー側で、非同期処理の開始と完了をログへ記録する
  • Suspense境界のフォールバックと実データの表示を、画面上で確認する

必要であれば、開発用の検証コードでFetchのReadableStreamを読み取り、受信したテキストを順番に記録する方法もあります。ただし、その場合もアプリケーションでRSC Payloadを自前パースするのではなく、ストリームがどのタイミングで到着しているかを観察する用途に限定したほうがよいでしょう。Wire Formatの内容を独自の業務ロジックへ結び付けると、Reactやフレームワークの更新で壊れやすくなります。

Chrome DevToolsだけで確認できることと、できないことを分けると、調査が安定します。

確認したいこと適した確認場所
RSC関連のリクエストが発生しているかNetworkのHeaders
レスポンス本文にどんなレコードがあるかNetworkのResponse
受信までの時間や接続状態NetworkのTiming、Waterfall
いつ描画が更新されたかPerformance、画面の状態
非同期処理がいつ終わったかサーバーログ
実装上どのコンポーネントが参照されるかビルド結果とフレームワークの開発者向け情報

RSC Payloadを読むときに混同しやすいこと

RSC Payloadを調べていると、HTML、JavaScript、データ通信の境界が曖昧に見えます。特にNext.jsのようなフレームワークでは、一つのページ表示に複数のリソースが関係するため、見えているレスポンスがすべてRSC Payloadとは限りません。

HTMLレスポンスとRSCレスポンスは同じとは限らない

初回のページ読み込みでは、ブラウザがHTMLを受け取り、その中に初期表示に必要な情報やRSC関連のデータが組み込まれることがあります。クライアント側のナビゲーションでは、HTML全体ではなく、ルートの変更に必要なRSCデータだけが取得される場合があります。

そのため、初回読み込みとクライアント遷移でNetwork上の見え方が違っても、不具合とは限りません。リクエストの種類、ヘッダー、レスポンスの内容、遷移方法をまとめて確認する必要があります。

RSC PayloadはAPIレスポンスの代用品ではない

RSC Payloadを見ていると、内部にデータが含まれているため、これを汎用APIのように利用したくなることがあります。しかし、RSC PayloadはReactのツリーを復元するための内部的な転送形式です。

別のクライアントや外部サービスへ提供するAPIとして設計されているわけではありません。データを再利用したい場合は、用途に合ったAPIレスポンスや共有のデータ取得層を用意したほうが、責務を分けやすくなります。

MやJを自分で生成しない

Wire Formatの例を読むと、MやJのレコードを自分で組み立てれば、Reactへ直接データを渡せるように見えるかもしれません。しかし、参照ID、モジュールマップ、Promiseの解決、エラー処理、バージョン差分などを自前で管理するのは現実的ではありません。

RSC Payloadは、Reactとフレームワークが生成・解釈する領域です。アプリケーション側では、サーバーコンポーネント、クライアントコンポーネント、Suspense、Propsの境界を正しく設計し、内部形式そのものには依存しないのが基本になります。

RSCのデータ転送を設計へ生かす

RSC Payloadの構造を理解すると、コンポーネントの分割方針も変わります。すべてをクライアントコンポーネントにするのではなく、サーバー側で完結する部分と、ブラウザ上で操作する部分を分けて考えられるようになります。

サーバーコンポーネントに向いているのは、データ取得、認証済みユーザーに応じた表示、静的なレイアウト、サーバー専用の処理です。クライアントコンポーネントに向いているのは、クリックや入力への反応、ブラウザAPIの利用、状態管理、ドラッグ操作などです。

両者をつなぐPropsでは、値の種類だけでなく、転送する必要性も確認します。大きなデータ構造をそのまま渡せば、クライアント側の処理やネットワーク転送が軽くなるとは限りません。必要な項目だけを選び、表示や操作に適した形へ整えることが重要です。

また、ストリーミングを導入する場合は、単にSuspenseで囲むだけでは不十分です。フォールバックが何を示すのか、どの処理が終われば実データへ置き換わるのか、ユーザーが待っている間に何を操作できるのかを考える必要があります。

RSCによる改善を確認するときも、TTFBだけ、あるいはTTIだけを見るのではなく、複数の段階を分けて観察します。

  • 最初のHTMLやPayloadが届くまで
  • レイアウトとフォールバックが描画されるまで
  • 実データを含む後続チャンクが届くまで
  • クライアントコンポーネントのJavaScriptが読み込まれるまで
  • 主要な操作が実際に反応するまで

この分解ができると、「表示は早いのにボタンがまだ動かない」「データは届いているのに画面更新が遅い」といった現象を、RSCのせいとして一括りにせず調べられます。

RSC Payloadの理解は、UI構築の地図になる

RSC Payloadは、HTMLを少し特殊にしたものでも、APIのJSONを複雑にしたものでもありません。サーバーで評価されたReactのコンポーネントツリーを、クライアント側で再利用・復元するための専用の転送表現です。

理解しておきたい要点は、次のように整理できます。

  • RSC Payloadは、React要素ツリー、Props、モジュール参照、Suspenseの状態を運ぶ
  • テキスト転送では行やレコード単位で処理されることがあるが、内部形式は実装依存である
  • MやJ、参照記号は、モジュールや要素、後続データを結び付ける役割を持つ
  • クライアントコンポーネントのコードは、Payloadではなくクライアント向けJavaScriptから解決される
  • Suspenseによって、準備できた部分と待機中の部分を分けて転送できる
  • フォールバックの早期表示は、ページ全体の操作可能時間が短くなることを意味しない
  • Date、Map、SetはRSCの対応範囲で扱える場合があり、JSON互換性だけで一律に判断しない
  • 正規表現、関数、循環参照を含む値は、別のプリミティブな表現へ変換する設計を検討する
  • Chrome DevToolsではNetworkのHeaders、Response、Timing、Waterfallを使って確認する

ブラウザでPayloadを見たとき、読みにくい記号や参照番号が並んでいても、すべてを手作業で解読する必要はありません。どのレコードがどの画面要素に対応するか、どのタイミングでSuspenseの内容が置き換わるか、クライアントコンポーネントのコードがどこから読み込まれるか。この三つを追うだけでも、RSCの動きはかなり具体的に見えてきます。

RSCを使う価値は、サーバーとクライアントの境界を曖昧にすることではありません。むしろ、どの処理をサーバーに置き、どの操作をブラウザに任せ、どのデータを境界越しに渡すのかを明確にできるところにあります。RSC Payloadとストリーミングは、その設計を実際の通信へ落とし込むための仕組みです。

よくある質問

RSCペイロードとは何ですか?
RSCペイロードは、サーバーで評価したReactのコンポーネントツリーをクライアント側で復元するための専用の転送表現です。要素ツリー、Props、クライアントコンポーネントのモジュール参照、Suspenseの状態などを含みます。
RSCペイロードはHTMLやJSONとどう違いますか?
HTMLのようにブラウザへ直接表示する完成済み文書でも、APIのJSONのようにアプリケーションが自由に解釈するデータモデルでもありません。Reactが要素ツリーや参照関係を解決し、UIを組み立てるためのデータです。
サーバーコンポーネントのコードはブラウザに送られますか?
サーバーコンポーネントの実装コードは、クライアントへ送って再実行する対象ではありません。データベースアクセスなどのサーバー専用処理をサーバー側に置き、クライアントコンポーネントだけをモジュール参照からクライアント向けJavaScriptに結び付けます。
SuspenseでRSCの表示はどのように進みますか?
非同期処理を待つ部分があると、サーバーは先に解決できるツリーやフォールバックを送ります。処理が完了すると、対応する境界の結果を後続チャンクで送り、クライアントがフォールバックを実際の内容へ更新します。
RSCペイロードはChrome DevToolsでどう確認できますか?
Networkパネルでページ文書や画面遷移時のFetch・XHRリクエストを確認し、Headers、Response、Timing、Waterfallを調べます。初回読み込みとクライアント遷移では、RSCデータの現れ方が異なる場合があります。

参考情報