2023年初頭、カナリーワーフを拠点とするフィンテックスタートアップのクライアントから、前のエージェンシーが構築したNext.js 13コードベースを渡されました。新しいapp/ディレクトリを使っていました。いいですね。でも、ハイドレーションエラーが至るところにあり、バンドルは340 KBの圧縮で、前のチームの誰もが「use client」を文字通りすべてのファイルに入れた理由を説明できませんでした。React Server Componentsについて尋ねると、彼らは「ああ、それを使っています」と言いました。彼らはそれを使っていませんでした。
これが今のRSCの問題です。みんなそれを理解していると主張しています。実際に理解している人はほぼいません。だから2023年初頭に存在していたはずの版を提供します。
RSCが実際に何であるか(マーケティング版ではなく)
React Server Componentsはサーバーでのみ実行され、JavaScriptをブラウザに送信しないコンポーネントです。以上です。
「サーバーサイドレンダリング」ではなく、「プリレンダリング」でもありません。これらのものはRSC以前に存在し、異なる方法で機能します。従来のSSR(Next.js pages routerが行うもの)では、コンポーネントはサーバー上でレンダリングしてHTMLを生成しますが、同じJavaScriptがクライアントに送信され、Reactは「ハイドレーション」を行い、イベントリスナーを接続し、ページを引き継ぎます。
RSCは2番目の部分を完全にスキップします。コンポーネントはサーバーで実行され、レンダリングされ、その出力をシリアル化されたペイロードとしてクライアントに送信し、その後...終わりです。そのコンポーネント用のJavaScriptがブラウザに到達することはありません。
実践的な効果:40 KBのマークダウンパーサーを使ってテキストをレンダリングする<ProductDescription>コンポーネントがあり、それをサーバーコンポーネントにした場合、その40 KBはユーザーに行きません。解析されたHTMLが行きます。以上です。
ReactチームのオリジナルRFCは、完全な根拠が欲しいなら実際に読む価値があります。密度が高いですが正直です。
ようやく納得できた思考モデル
コンポーネントツリーを、たまたま一緒に縫い合わされた2つの分離された世界だと考えてください。
World 1(サーバー)。データベース、ファイルシステム、環境変数、シークレットに完全にアクセスできます。useStateやuseEffect、またはブラウザAPIは使えません。イベントリスナーを付与することもできません。
World 2(クライアント)。ブラウザで実行されます。すべてのReactフックが使えます。データベースに直接通信することはできません。代わりにAPIにリクエストを送ります。
RSCの前は、すべてのコンポーネントがWorld 2に存在していました。たとえサーバーで最初にレンダリングされたとしても。RSCを使えば、コンポーネントを明示的にWorld 1に配置できるようになりました。これらのコンポーネントはWorld 2のコンポーネントを子としてレンダリングでき、シリアライズ可能なpropsを渡せます。ただし、World 2のコンポーネントはWorld 1のコンポーネントをレンダリングできません。境界は一方向です。
ここで混乱する人が多いです。サーバーコンポーネントにするためにディレクティブを追加するわけではありません。Next.jsのapp/ディレクトリでは、デフォルトですべてがサーバーコンポーネント です。クライアントに使う場合は「use client」でオプトインします。ほとんどの人の想定と逆です。
Seahawkは昨年、イギリスの小売業者向けの大規模eコマースカタログプロジェクトに取り組みました。約60個のコンポーネントを監査したところ、そのうち約40個が理由もなく「use client」になっていました。それを削除したら、JavaScriptバンドルが約28%削減されました。1日の午後の作業でした。
できることとできないこと(具体的な内訳)
サーバーコンポーネントができること:
- async/awaitで直接データをフェッチします。useEffectもローディング状態もありません。ただconst data = await db.query(...)です。
- 重いサーバーのみの必要なライブラリ(frontmatterパースのgray-matterやPDFジェネレータなど)をインポートしても、クライアントバンドルには影響しません。
- Nodeの
fsモジュールでファイルシステムから読み込みます。 - ブラウザに公開したくない環境変数にアクセスします。
- データをpropsとしてクライアントコンポーネントに下に渡します。
サーバーコンポーネントができないこと:
- useStateまたはuseReducerを使う
- useEffectまたはuseLayoutEffectを使う
- イベントハンドラを付与する(onClickやonChangeなど)
- ブラウザAPI(window、document、localStorage)を使う
- Reactコンテキストを直接使う(ただし回避パターンはあります)
クライアントコンポーネントはサーバーコンポーネントができないことすべてはできますが、以下の制限があります:
- データベースを直接呼び出すことはできません。
- サーバーのみの必要なパッケージは使えません。
- コードがブラウザに配信される
この表現はパフォーマンスだけの話ではありません。コードがどこで実行され、何にアクセスできるかについてです。「高速」対「低速」という考え方よりも、この枠組みの方がはるかに有用です。
データ取得こそが RSC が本領を発揮する場面
ここが本当に気に入っているところです。RSC 以前は、Next.js アプリでデータを取得するなら通常、getServerSideProps、getStaticProps、クライアント側の useEffect 呼び出し、またはこれら三つすべての組み合わせにカスタムフックで読み込み状態を管理するというのが相場でした。ごちゃごちゃしていたわけです。
RSC なら、単に...取得するだけです。コンポーネント内で。最上位レベルで。
`` async function ProductPage({ id }) { const product = await getProduct(id); // calls your DB directly return <ProductDetails product={product} />; } ``
ページレベルの関数からのプロップ掘り下げもありません。到着時に準備できていたはずのデータ用の読み込みスピナーもありません。コンポーネントは非同期で、必要なものを待って、レンダリングします。
ここから本当に面白くなります。各サーバーコンポーネントが独立してデータを取得できるので、昔の「ゴッドコンポーネント」問題を回避できます。そこではトップレベルの一つの関数がページ全体のすべてのデータを調整しなければなりませんでした。コンポーネントは自己完結します。Promise.all() を待つとき、または兄弟コンポーネントが独立して取得するときに、並列取得は自然に起こります。
去年の春、バーミンガムのロジスティクス企業のダッシュボードプロジェクトでこのパターンを使いました。各ウィジェット、出荷統計、遅延アラート、コスト概要はそれぞれが独自の非同期サーバーコンポーネントでした。共有される読み込み状態もなく、プロップ掘り下げもなく、競合状態もありません。ページの LCP は 2.1 秒から 0.9 秒に改善しました。RSC だけが理由ではなく、RSC がより良いアーキテクチャに到達するのをはるかに簡単にしたのです。
レンダリングパイプライン(実際に何が起こるか)
ユーザーが app/ ルータを使用する Next.js 14 アプリのページをリクエストすると、ほぼこのような流れになります:
- Next.js はサーバー上でサーバーコンポーネントを実行します
- これらのコンポーネントは、生の HTML ではなく React Server Component Payload(RSC Payload)と呼ばれる特殊な形式を生成します。これは UI ツリーの直列化された説明です
- Next.js はそのペイロードを使用して初期 HTML を生成します(最初のペイントのために)
- その HTML はブラウザに送信されます
- RSC Payload も送信され、クライアント React ランタイムはそれを使用してツリー内のクライアントコンポーネントだけをハイドレートします
- クライアントコンポーネントは JavaScript を取得し、ハイドレートされ、対話的になります
ステップ 5 が RSC を単なる SSR と異なるものにします。クライアントはサーバーコンポーネントを再レンダリングしません。ペイロードを使用して全体像を埋め、対話的なビットにハイドレーション作業を集中させるだけです。
Next.js のレンダリングに関するドキュメントは、深掘りしたい場合、ほとんどのブログ記事より詳しく説明しています。
RSC が破綻する場面(正直な批判)
そうですね。これがすべて楽観的だと思わないようにしましょう。
「"use client"」境界がカスケードします。"use client" をコンポーネントに配置した瞬間、それがインポートするすべてのコンポーネントもクライアント側になります。注意しないと、一つの onClick ハンドラが驚くほど大きなサブツリーをクライアントバンドルに引き込みます。チーム内の新人開発者がこれで何度も引っかかるのを見てきました。
Context は厄介です。React Context はサーバーコンポーネントでは機能しません。テーマ、認証状態、またはグローバル設定のために Context 中心にアプリアーキテクチャを構築している場合は、リストラクチャが必要になります。回避策があります(値をプロップとして渡す、クライアントラッパーを使う)が、以前はなかった摩擦です。
デバッグはより難しいです。サーバーコンポーネントのエラーはブラウザコンソールで同じようには表示されません。エラー境界の動作が異なります。サーバーログを確認する必要があります。致命的ではありませんが、すべてのエラーが DevTools 内にある習慣があれば、学習曲線を覚悟してください。
サードパーティライブラリはしばしば対応していません。フック下またはブラウザ API を内部で使用するライブラリはサーバーコンポーネントで壊れます。日付ピッカーまたはアニメーションライブラリを使用するために "use client" ファイルに包装するのに時間を費やします。React エコシステムは追いついていますが、2024 年時点ではまだ不均一です。
正直なところ、シンプルなマーケティングサイトやコンテンツブログなら、RSCは必ずしも必要ではありません。チームがpages routerに慣れていて、パフォーマンスに問題がなければ、RSCサポートのためだけにアップグレードする価値はありません。移行を勧めないようにしたクライアントは一度や二度ではありません。
既存プロジェクトへのRSC導入に関する実践的なアドバイス
既存のNext.jsアプリをapp/ディレクトリに移行する場合、以下の順序に従います。
- リーフコンポーネントから始めてください。データを表示しますがインタラクションを処理しないコンポーネントが最も簡単に成功します。まずサーバーコンポーネントにしてください。
- 実際のインタラクティビティの領域を特定してください。通常、思っているより小さいです。フォーム、モーダル、ドロップダウン。これが「use client」の領域です。
- `"use client"`をできるだけ下位に配置してください。フォームを送信するボタンはクライアントコンポーネントであるべきです。その周りのフォームレイアウトはおそらく必要ありません。
- [@next/bundle-analyzer](https://www.npmjs.com/package/@next/bundle-analyzer)でバンドルを監査してください。前後で実行してください。意味のあるバンドルサイズの削減が見られていない場合、おそらくまだクライアントに多くを送信しています。
- サーバーのみのモジュールを共有しないでください。npmからserver-onlyをインストールし、ブラウザに到達してはいけないモジュールの先頭でインポートしてください。何かがクライアント側からインポートしようとすればビルド時にエラーが発生します。
server-onlyパッケージは小さなものですが、ヘルスケアクライアントプロジェクトでかなり恥ずかしいデータ漏洩シナリオを防いでくれました。開発者が誤ってデータベースユーティリティを後で「use client」とマークされたコンポーネントにインポートしました。このパッケージはビルド時にそれを検出しました。使ってください。
FAQ
React Server ComponentsはSSRと同じですか?
いいえ。SSR(サーバーサイドレンダリング)はコンポーネントをサーバーでHTMLにレンダリングしますが、それでもコンポーネントのJavaScriptをハイドレーション用にクライアントに送信します。RSCはサーバーでレンダリングし、JavaScriptをブラウザに送信しません。SSRとRSCは連携でき、Next.jsのapp/ディレクトリではそうしていますが、異なる問題を解決します。
React Server Componentsを使用するにはNext.jsが必要ですか?
技術的にはいいえですが、ほとんどのチームにとって実際にはそうです。RSCはサーバーインフラストラクチャ、ルーティング、およびRSCペイロードパイプラインを処理するフレームワークが必要です。app/ディレクトリを備えたNext.js 13以降が現在最も本番環境対応のオプションです。Remixは異なるモデルを採用しています。独自のセットアップをロールアウトすることは可能ですが、フレームワーク自体を構築している場合を除き、時間を無駄にしない方がいいです。
同じページでサーバーコンポーネントとクライアントコンポーネントを混在させることができますか?
はい、それが要点です。ページはサーバーコンポーネント(データをフェッチ、JSは送信されない)で、検索入力用のクライアントコンポーネントを含み、それはchildrenプロップを通じて渡される子としてサーバーコンポーネントを含むかもしれません。ツリーはインターリーブされます。覚えておいてください。サーバーコンポーネントはクライアントコンポーネントをレンダリングできますが、クライアントコンポーネントはサーバーコンポーネントを直接レンダリングできません。
既存のカスタムフックはどうなりますか?
useStateやuseEffect、またはブラウザAPIを使用するカスタムフックはクライアントコンポーネントでのみ実行できます。それらを書き直す必要はありません。どのコンポーネントがそれらを使用するかについて意図的であるだけです。フックがデータフェッチの呼び出しをラップしている場合、そのデータをサーバーコンポーネントで代わりにフェッチしてプロップとして渡すことができるかどうかを検討してください。しばしばそうすることができ、よりシンプルなコードになります。
RSCペイロード形式にはパフォーマンスコストがありますか?
ペイロードを通じて大量のデータを渡している場合、特にそうする可能性があります。RSCペイロードはJSONと同じではなく、カスタム形式ですが、それでも応答にバイトを追加します。ほとんどのアプリでは、これはJSバンドルの削減と比べて無視できます。プロップとして渡されている非常に大規模なデータセットを持つアプリの場合、プロファイルしてください。RSCが常にネット上で小さいと仮定しないでください。
---
RSCは単なる新しいAPIではなく、真の建築上のシフトです。メンタルモデルは落ち着くのに時間がかかります。その時間をあげてください。大きなクライアントプロジェクトにコミットする前に、app/ディレクトリで何か小さなものを構築してください。本番環境にRSCベースのアーキテクチャを出荷できることを信頼できるようになるまで、約3週間の実験を行いました。2016年からReactを書いていたのに。
実際にそれで何かを構築していない人たちからの手振りは続くでしょう。少なくともこれで違いを見分けるのに十分な知識があります。
