🐦

「RSCはJSサーバを必要としない」話の実例と易しい解説

2024/01/14に公開
4

先日(2024/1/12)、Remix v2.5.0で追加されるSPAモードに関する話題の中でDan(@dan_abramov2)がReact Server Component(RSC)について以下のようなポストをしていました。
https://x.com/dan_abramov2/status/1745794290679259635?s=20

Twitter(X...?知らない子ですね...)上ではこのポストに混乱した人が続出しDanが熱心に説明している様です。

Danの説明は私のこれまでの理解と一致しているのですが、イマイチ腑に落ちていない方も多いのかなと思い実際の例を見ながら簡単に説明してみようと思います。
ちなみにuhyoさんの↓の記事も今回の話を理解する上で非常に参考になると思うので、目を通していただくと良いかと思います。
https://zenn.dev/uhyo/articles/react-server-components-multi-stage

要約

  • Next.jsでApp Routerをつかってstatic exportすると色々分かるよ
  • RSCは「事前にどこかでコンポーネントの計算を済ませておいて上手いこと使うための仕組み」でしかないよ
  • だからNode.jsとかのJSランタイムを持ったHTTPサーバはRSCにとって必須じゃないよ、目的に応じて選択できるよ

サンプルを見る

丁度良いところに昨年公開したものの更新が捗っていない私のブログサイトがあるので今回はこちらを使って説明していきます。
https://blog.shin-taro.info/

このサイトはNext.js(App Router)を使用しstatic exportしたファイルを静的に配信しています。

ブラウザからサイトにアクセスするとまずは該当ページの完全なHTMLが返却されます。

続いてクライアントサイドで動作させるjsファイルとその他CSSやfontファイルなどが読み込まれます。Hydrationも行われます。

その後別のページに遷移する際には遷移先のページ表示に必要なjsファイルと.txt形式のファイル(RSCプロトコルによってシリアライズされたコンポーネントです。後で詳しく解説します。)がprefetchにより読み込まれ、リンクを押下したタイミングでクライアントサイドでレンダリングを行い画面が切り替わります。

このサイトはまさに "RSCを使用しているが、JSランタイムを搭載したサーバは使用していない" 状態です。

ちなみに上記の一連の流れでRSCが直接関係しているといえる箇所は ".txt形式のファイル" の存在だけで、それ以外のほとんどはNext.jsに機能によるものとRSC以前からのReactの挙動です。

では、細かい挙動を見ながらRSCが何なのか(あるいは何をしないのか)を見ていきます。

buildしてみる

では、とりあえずbuildしてみましょう。
output: "export"にしてnext buildします。

結果がこちらです。

当然全てのページがstaticに出力されていますね。

で、こちらが出力先のディレクトリ

各ページもそれぞれHTMLファイルが出力されています。

そしてこの辺がクライアントサイドで動くjsファイル達です。

何が起こったのか

まず出力されたそれぞれのファイルが何なのか見ていきます。

HTMLファイル

これは Next.jsが Reactコンポーネントをレンダリングすることによって出力されたHTMLです。
基本的には Server ComponentもClient ComponentもHTMLとして出力されます。
(今回はありませんが、Suspense等の例外はあり得ますが本筋から逸れるため割愛します)
これによって空のHTMLに0からクライアントサイドでReactがDOMを構築するよりも早く表示させることができます。
このファイルは外部サイトから遷移してきたり、直接URLをたたいたりといった初回アクセス時のみブラウザに送られます(送るのは任意のHTTPサーバです)。

重要なのはHTMLファイルとして出力されることと、 RSCは直接的には関係ありません

jsファイル

これらはReact本体と、我々開発者が書いたコードをbundle・minifyなどしたコードです。
ざっくり「Reactがブラウザ上にUI表示するために必要なコード」ですね。
全てのページ共通で使用されるもの、各ページ毎のものがそれぞれ存在しており、このコード分割はNext.jsが行っています。

このJavaScriptのコードはすべてユーザーのブラウザで実行され、サーバーサイドでは実行されません。

ちなみにすべてServer Componentのページであっても少量のJavaScriptコードが必要になります。
ただし、Server Componetはこの時点で計算を済ませているためコンポーネント本体(?)の情報は含まれません。
Server Componentの情報は次に説明する.txt形式で出力されたファイルにあります。
この部分がRSCの話になります。やっと出てきましたね。

txtファイル

Server Componentを計算しシリアライズした結果のRSC Payloadと呼ばれるモノです。
別の画面に遷移する時など、対象のコンポーネントが必要になったタイミングでfetchされコンポーネントツリーに追加され表示されます。

私が書いたFunction Componentはbuildの時点で計算を済まされ、RSC Payloadに変換されています。必要に応じてdata fetchも行われています。
つまりそのコンポーネントは計算が確定した状態でtxtファイルになっているため、ユーザーのブラウザではコンポーネントの計算をスキップしてほぼそのまま使えるような状態なわけです。
("コンポーネント"という言葉はふわっと使っているので雰囲気で読んでください)

このあたりで話が見えてきたと思いますが、Node.jsのようなランタイムを持ったサーバを持っていなくてもbuild時にServer Componentは計算を終えブラウザでは実行されないというようにRSCの恩恵を受けることができています。

尚、.txtファイルの形式になっているのはstatic exportしているからでdev serverの場合やon-demandでdata fetchを行うような場合(所謂SSR)等は別の形式になったりするようです。

つまり?

まとめると

  • buildするとHTMLファイルやjsファイルtxtファイルなどが生成される
  • HTMLファイルやjsファイルの出力については基本的にNext.js依存の処理
  • RSCを活用することによってServer Componetはこの時点で計算・シリアライズされjsファイルにはコンポーネント自体の情報はない

あとはブラウザでNetworkタブとか開きながら見ていただけると概ね理解できるのではないかと思います。

SSRはどうなの?

「じゃあJSのサーバーを用意してSSRしたりするときはどうなの?」
「static exportしてるから特殊なことが起こってるんじゃないの?」

と思った方向けに補足します。

JSサーバーを用意してon-demandでレンダリングを行う場合もRSCの立場は変わりません。
今回buildしたときに起こった "Reactコンポーネントを計算してRSC PayloadやHTMLを生成する" という一連の処理について、どこからどこまでをどのタイミングでどの場所で行うか が変わるだけです。

例えば「全てのコンポーネントをリクエスト時に最新のデータを取得してレンダリングする」とした場合、ざっくり以下のような流れになります。

  • build時にはNext.jsによってjsxのコンパイル・バンドル・コード分割・minify等が行われ、コンポーネントの計算やデータの取得は行われない
  • リクエストに応答し、HTMLやJSONなどを返すためのサーバーサイドのスクリプトが生成される
  • デプロイ
  • サイトに訪問するためのリクエストが来ると上記のスクリプトによってデータを取得し、そのページのコンポーネントをすべてレンダリングしHTMLを生成し返す(Suspenseされたものを除く)
  • この時クライアントサイドのJavaScriptも返却されHydrationが行われるが、Server Componetはすでに計算を終えているのでその結果しか含まれない。
  • 画面遷移時(厳密にはprefetch時)にはリクエストが飛びサーバサイドのスクリプトによってデータ取得とServer Componentの計算とシリアライズが行われる。
  • ブラウザにはClient Componentなどが含まれたクライアントサイド用のJavaScriptとシリアライズされたServer Componentの計算結果が返される。

RSCが直接関係しているところを太字にしましたが、static exportしていた時はbuild時に行われていた一連の処理がリクエスト時にサーバー上で行われているだけであることが分かるかと思います。
つまりタイミング場所が変わっているだけなわけです。
Next.jsのApp Routerでキャッシュが絡んでくる場合も、結局キャッシュの寿命によってタイミングが変わるだけなので同じことです。

そしてこれらはフレームワークや開発者が選択するものであり、RSC自体が "JSサーバーを必要としている" 訳では無いということです。

まとめ

長くなっちゃいましたが、RSCは単に「事前にどこかでコンポーネントの計算を済ませておいて上手いこと使うための仕組み」でしかないということでした。
「事前にどこかで」の部分はRSCによって規定されたものがあるわけではなく、フレームワークや開発者が選択できるものだよ、だからJSのサーバーは"optional"だよ。ってことです。

Discussion

EdamAmexEdamAmex

細かいですが
Htmlファイルの章のSuspenseをtypoしています

shin-taroshin-taro

ご指摘ありがとうございます!該当箇所は修正いたしましたmm