ガイドこのページを編集.md

アーキテクチャのトレードオフ

このドキュメントでは、主要なアーキテクチャ上の決定事項、それらを形作った制約、および受け入れられたトレードオフについて説明します。ランタイムモデルとReact Server Componentsに精通していることを前提としています。

@lazarv/react-server は、レイヤー構造のランタイムとして設計されています:

実行基盤
↓ 実行環境とレンダリングパイプラインを定義

実行拡張
↓ 追加の実行コンテキストを導入

アプリケーションモデル
↓ アプリケーションの構造化方法を形成

運用モデル
↓ デプロイと本番環境の動作を管理

各上位レイヤーは、下位レイヤーの保証に依存しています。

これらの決定は、ランタイムの最下層を定義します:使用するReactのバージョン、サポートするJavaScriptランタイム、モジュールの解決方法、使用するビルドツール、そしてコアとなるレンダリングおよびハイドレーションパイプラインの動作です。上位のすべてはこれらの選択に依存しています。

問題: React Server Componentsは、react-server-dom-webpackとReactリコンサイラー間の不安定なワイヤーフォーマットに依存しています。シリアライゼーションプロトコル、フライトフォーマット、およびディレクティブのセマンティクス("use client""use server")は、Reactのパブリックsemverコントラクトではまだカバーされていません。ランタイムが使用するReactバージョンとアプリケーションがインストールするバージョンの不一致は、サイレントなシリアライゼーション失敗やランタイムクラッシュを引き起こします。

制約: RSCワイヤーフォーマットは、サーバーレンダラー、クライアントハイドレーター、およびフライトエンコーダー/デコーダー間でバージョンロックされている必要があります。ユーザーがインストールしたReactを許可すると、Reactが頻繁に内部的な破壊的変更を含む実験的ビルドを公開するため、信頼性のあるテストができない組み合わせの互換性表面が生まれます。

決定: 特定のReact実験的ビルドを直接の依存関係としてバンドルします。起動時に、reactreact-domreact/jsx-runtime、およびreact-server-dom-webpackのすべてのインポートがランタイムにバンドルされたコピーに解決されるようにモジュール解決をハイジャックします。Node.jsではカスタムローダーを使用したmodule.register()を、Bunではmodule.mock()/module.alias()を、Denoではインポートマップをディスクに書き込み--import-mapでプロセスを再起動します。

トレードオフ:

結果: ワイヤーフォーマットはすべてのレンダリングパスで一貫性が保証されます。ユーザーはReactをインストールする必要がなく、依存関係の競合やバージョンのずれが軽減されます。ただし、ランタイムのリリースサイクルがReactのアップグレードのボトルネックとなり、3方向のエイリアスレイヤーは各ランタイムのリリースサイクルに対して検証が必要なメンテナンス対象となります。

問題: JavaScriptサーバーエコシステムは、Node.js、Bun、Denoに分断されています。各ランタイムは異なるモジュール解決セマンティクス、ワーカースレッド、ファイルシステムアクセス、プロセス管理のAPIを持っています。Node.jsのみをターゲットにすると、増加するデプロイメントの一部が除外されます。

制約: ランタイム抽象化は、モジュールエイリアス(固定されたReactバージョンを参照)、プロセスレベルのAPI(cwdargvexit、環境変数)、および並行処理プリミティブ(setImmediatesetTimeoutBufferUint8Array)をカバーする必要があります。エッジ環境(Cloudflare WorkersNetlify EdgeDeno Deploy)はnode:ビルトインモジュールが一切利用できません。

決定: すべてのランタイム依存APIを、グローバル検出(typeof Denotypeof Buntypeof EdgeRuntime)に基づく条件付き実装を持つシステムレイヤー(sys.mjs)の背後に抽象化します。各ランタイムは独自のモジュールエイリアス戦略を持ちます。エッジ環境は別途検出され、ファイルシステム、ワーカースレッド、クラスターモードのない制限されたサブセットとして扱われます。

トレードオフ:

結果: 同じアプリケーションコードがNode.js、Bun、Denoでコード変更なしに実行されます。エッジデプロイメントは制限された実行モードを使用します。ただし、マルチランタイムの対応範囲は、ランタイム固有のリグレッションが発生しやすく、環境間での再現が困難であることを意味します。

問題: Node.jsエコシステムは歴史的にCommonJSに依存してきました。CJSモジュールは同期的であり、ツリーシェイキングのための静的解析ができず、ブラウザネイティブのモジュール読み込みとの摩擦を生みます。ツールにおけるCJS/ESMの二重サポートは複雑さとエッジケースを追加します。

制約: ViteはESMでネイティブに動作します。React Server Componentsの"use client"および"use server"ディレクティブのモジュール解決は、静的インポート解析に依存しています。CJSのサポートは両方の目標を損なう互換性レイヤーを必要とします。

決定: スタック全体(サーバー、クライアント、ビルドツール)でESMのみをターゲットにします。

トレードオフ:

結果: 静的解析が信頼でき、正確なツリーシェイキングとディレクティブ検出が可能になります。モジュールグラフは開発と本番で一貫しています。ブラウザネイティブのimport()がモジュールフォーマット変換レイヤーなしで動作します。

問題: カスタムバンドラーと開発サーバーをゼロから構築するのは、膨大なエンジニアリング作業です。Webpackベースのソリューションは設定のオーバーヘッドが大きく、リビルド時間が遅くなります。ツールレイヤーは、HMRサポートを備えたサーバーとクライアントの両方のモジュールグラフを処理する必要があります。

制約: 選択するツールは、ESMをネイティブにサポートし、RSCモジュール解決を実装するのに十分な拡張性を持つプラグインAPIを提供し、SSRをそのままサポートする必要があります。また、活発なエコシステムとコミュニティを持つ必要があります。

決定: Viteをビルドおよび開発の基盤として使用します。ViteのRollupベースの本番ビルドとesbuild/Rolldown搭載の開発サーバーが、必要なESMファーストアーキテクチャを提供します。

トレードオフ:

結果: HMRレイテンシーはフルリバンドルではなくViteの変換パイプラインによって制限されますが、実際の速度はモジュールグラフのサイズとプラグインの数に依存します。Viteプラグインインターフェースが直接使用されます — ViteやRollupプラグインをビルドに組み込むためのアダプターやラッパーは不要です。これによりランタイムのリリースサイクルがViteに結合され、ViteのSSRレイヤーのリグレッションが継承されます。ViteのWhy Viteドキュメントも参照してください。

問題: 従来のSSRアプローチは、完全にレンダリングされたHTMLを送信し、ハイドレーション中にクライアント上でコンポーネントツリー全体を再実行します。これは作業の重複を生み、不要なJavaScriptを配信し、UIのどの部分が静的でどの部分がインタラクティブかについて開発者のコントロールが限定的です。

制約: サーバーとクライアント間のレンダリング境界は、コンポーネントレベルで明示的である必要があります。ReactのServer Componentsプロトコルは、サーバーコンポーネントツリーのシリアライゼーションとクライアントコンポーネントコードの選択的配信をランタイムがオーケストレーションすることを要求します。

決定: React Server Componentsをプライマリレンダリングモデルとして採用します。コンポーネントはデフォルトでサーバーレンダリングされます。クライアントコンポーネント"use client"ディレクティブによるオプトインであり、そのコードのみがブラウザに送信されます。

"use client"; import { useState } from "react"; export default function Counter() { const [count, setCount] = useState(0); return ( <button onClick={() => setCount(count + 1)}>Count: {count}</button> ); }

トレードオフ:

結果: 完全な静的ページではクライアントサイドJavaScriptをゼロにすることが可能です。インタラクティブなアイランドは明示的にスコープされます。ランタイムがシリアライゼーションフォーマットを制御するため、固定されたReactバージョンが必要です(以下を参照)。

問題: 従来のSSRは、クライアントに送信する前に完全なHTMLレスポンスをバッファリングします。遅いデータソースを持つページはレスポンス全体をブロックし、最初のバイトまでの時間と体感レイテンシーが増加します。

制約: サーバーは利用可能になったHTMLを順次送信する必要があります。サーバー上のSuspense境界がチャンク分割ポイントを定義します — Suspenseフォールバック内のコンテンツは、非同期処理が完了するとストリーミングされます。ブラウザは受信したチャンクを段階的にレンダリングできる必要があります。

決定: デフォルトでストリーミングSSRを使用します。シェルと最初に解決されたSuspense境界の準備ができ次第、レスポンスのフラッシュが開始されます。後続のチャンクはデータの解決に応じて配信されます。

import { Suspense } from "react"; async function AsyncData() { const data = await getData(); return <div>{data}</div>; } export default function App() { return ( <Suspense fallback={<div>Loading...</div>}> <AsyncData /> </Suspense> ); }

トレードオフ:

結果: 最初のバイトまでの時間は、ページの最も遅い部分ではなく、最も速い部分によって決定されます。遅いデータソースは初期描画をブロックしません。Suspense境界はコンポーネントツリーにおける明示的なローディング状態の宣言として機能します。

問題: フルページハイドレーションは、SSR後にクライアントサイドのコンポーネントツリー全体を再実行します。これはまだ表示されていない、またはインタラクティブでないページの部分に対してもCPU時間を浪費し、インタラクティビティを遅延させます。

制約: ハイドレーションはストリーミングモデルと整合する必要があります — チャンクが到着しクライアントコンポーネントがDOMにレンダリングされると、ページ全体を待つことなくインタラクティブになる必要があります。Reactランタイムの選択的ハイドレーションメカニズムがこれを処理しますが、サーバーはコンポーネント境界を正しい順序で出力する必要があります。

決定: Reactの組み込みの選択的ハイドレーションに依存します。クライアントコンポーネントはHTMLとJavaScriptの到着に応じて独立してハイドレートされます。カスタムハイドレーションスケジューラーは追加されません。

トレードオフ:

結果: コンポーネントは、兄弟や親を待つことなく、コードが到着し次第インタラクティブになります。ストリーミングと組み合わせることで、ページは段階的にレンダリングとハイドレーションの両方が行われます。コンポーネントごとのバンドルサイズがインタラクティブになるまでの時間に直接影響します。

これらの決定は、基本のレンダリングモデルを追加の実行コンテキストに拡張します。それぞれがコンポーネントの実行場所や実行方法を変更する新しいディレクティブまたはプリミティブを導入します — リアルタイムプッシュ、バックグラウンドスレッド、クロスオリジン合成、ハイブリッドレンダリング境界、およびサーバー/クライアントミューテーションインターフェースです。

問題: 従来のRSCレンダリングはリクエスト-レスポンス方式です:サーバーは一度レンダリングして結果を送信します。サーバーコンポーネントが時間の経過とともにクライアントに更新をプッシュする組み込みメカニズムはありません — リアルタイムダッシュボード、フィード、モニタリングUIは、コンポーネントモデル外の別個のWebSocketインフラストラクチャを必要とします。

制約: 更新はReactツリーでなければなりません — クライアントは生データではなく、レンダリング可能なRSCペイロードを受信する必要があります。各コンポーネントインスタンスには、独立したライフサイクル管理を持つ独自のサーバーサイド実行コンテキストが必要です。トランスポートはSPAでのページレベルナビゲーションを生き延びる必要があります。

決定: "use live"ディレクティブを導入します。このディレクティブでマークされた非同期ジェネレーター関数はライブコンポーネントにコンパイルされます。最初のyieldは通常のSSRレスポンスの一部としてレンダリングされます。

"use live"; export default async function* Clock() { while (true) { yield <div>Current time: {new Date().toLocaleTimeString()}</div>; await new Promise((resolve) => setTimeout(resolve, 1000)); } }

初期レンダリング後、後続のyieldはRSCフライトペイロードとしてシリアライズされ、Socket.IO接続を介してクライアントにプッシュされます。各コンポーネントインスタンスは独自のSocket.IOネームスペースとAbortControllerを取得します。

トレードオフ:

結果: リアルタイムUI更新は、命令的なWebSocketメッセージハンドラーではなく、yieldを持つReactコンポーネントとして表現されます。コンポーネントモデルはエンドツーエンドで保持されます。ただし、ライブコンポーネントは長時間実行サーバー環境に制限され、インスタンスごとのリソースコストが同時ライブコンポーネントインスタンスの最大数を制限します。

問題: CPU集約的な操作(画像処理、データ変換、圧縮)はサーバー上でメインイベントループをブロックし、すべての同時ユーザーのリクエストスループットを低下させます。この処理を別のプロセスやサービスに移すと、デプロイメントとオーケストレーションの複雑さが増します。

制約: オフローディングメカニズムはRSCシリアライゼーションプロトコルと統合する必要があります — 引数と戻り値(Reactエレメントを含む)はフライトフォーマットを介してワーカー境界を越える必要があります。サーバーではnode:worker_threads、ブラウザではWeb Workersを意味します。エッジランタイムにはどちらもありません。

決定: "use worker"ディレクティブを導入します。Viteプラグインがモジュールをプロキシに書き換えます:Node.js/Bunでは、各呼び出しがRSCフライトプロトコルでシリアライズされた引数とpostMessageで転送されるWorkerスレッドを生成または再利用します。

"use worker"; export async function computeFactorial(n) { if (n <= 1) return 1; return n * computeFactorial(n - 1); }

ブラウザでは、同じディレクティブがWeb Workerインポートを生成します。エッジランタイムでは、プロキシは同期的なインプロセス呼び出しにフォールバックします — 実際の並列性はありません。

トレードオフ:

結果: 重い計算はNode.jsおよびBunでメインイベントループから離れて実行されます。ReactエレメントをワーカーからDirectに返すことができます。ただし、利点は環境に依存し、シリアライゼーションコストは呼び出しごとに発生し、ワーカーキャッシュはグローバルなミュータブルリソースです。

問題: 大規模な組織は、フロントエンドアプリケーションを複数のチームとリポジトリにまたがってデプロイします。独立してデプロイされたUIをコンポーネントレベルで合成すること — iframeやクライアントサイドモジュールフェデレーションを介したページレベルではなく — には、共有レンダリングプロトコルと依存関係の重複排除が必要です。

制約: リモートコンポーネントは、ホストアプリケーションが消費可能なRSCフライトペイロードを生成する必要があります。リモートアプリケーションからのクライアントサイドJavaScriptは、ブラウザで共有依存関係(React、react-dom)を重複させてはいけません。合成は、特別なクライアントサイドグルーコードなしで、ストリーミングおよびハイドレーションパイプラインと連携する必要があります。

決定: レンダリング時にリモートのreact-serverインスタンスからRSCフライトデータをフェッチする<RemoteComponent src="..." />プリミティブを提供します。

import RemoteComponent from "@lazarv/react-server/remote"; export default function Home() { return ( <div> <h1>Host application</h1> <RemoteComponent src="http://localhost:3001" /> </div> ); }

ホストはリモートのブラウザマニフェストをフェッチし、自身のマニフェストとクロスリファレンスし、共有依存関係のリモートのクライアントモジュールURLをホストのコピーにリマップするインライン<script type="importmap">を出力します。各リモートサブツリーは、独立したハイドレーションとリフレッシュのための独自のアウトレットIDを取得します。

トレードオフ:

結果: 独立してデプロイされたReactアプリケーションが、共有依存関係の重複排除を伴いコンポーネントレベルで合成されます。ただし、システムは非react-serverリモートには閉じており、ホストとリモート間のバージョン結合は暗黙的であり、インポートマップ生成はレンダリングごとにオーバーヘッドを追加します。

問題: 完全な静的生成は高速な初期ロードを生成しますが、パーソナライズされたコンテンツや時間的に敏感なコンテンツを提供できません。完全なサーバーレンダリングは動的コンテンツを処理しますが、最初のバイトまでの時間が長くなります。ページレベルで2つのどちらかを選ぶのは粒度が粗すぎます — 多くのページには静的なシェルと小さな動的領域があります。

制約: コンポーネントツリーは、開発者がページを別々のエンドポイントに再構築することなく、コンポーネントレベルで静的部分と動的部分に分割可能でなければなりません。静的シェルはビルド時にプリレンダリング可能でCDNから配信可能でなければなりません。動的部分はリクエスト時に同じレンダリングパイプラインを使用してストリーミングされる必要があります。

決定: "use dynamic""use static"ディレクティブを導入します。"use dynamic"はReactのストリーミングレンダラーが境界として解釈するpostponeシグナルを注入します — 静的シェルがその周りにレンダリングされ、動的コンテンツがリクエスト時にストリーミングされます。

import { Suspense } from "react"; async function UserGreeting() { "use dynamic"; const user = await getUser(); return <p>Welcome, {user.name}</p>; } export default function Page() { return ( <div> <h1>Dashboard</h1> <Suspense fallback={<p>Loading...</p>}> <UserGreeting /> </Suspense> </div> ); }

"use static"は、プリレンダリング対応のメモリキャッシュドライバーを持つ"use cache: static"のエイリアスです。ビルド時に、ランタイムはpostpone境界を含むページのプリレンダリングされたHTMLと共に.postponed.jsonファイルを出力します。アダプターはこれらのファイルを使用して、動的部分のストリーミングレンダラーにリクエストをルーティングします。

トレードオフ:

結果: ページはCDNから静的シェルを即座に配信し、動的領域はリクエスト時にストリーミングされます。ただし、この機能はアダプターのサポートに依存し、不安定なReact内部を使用し、2つのディレクティブは対称的なネーミングにもかかわらず非対称な実装パス(postpone対キャッシュ)を持っています。

問題: 従来のWebアプリケーションは、クライアントからのミューテーションを処理するために別のAPIレイヤー(REST、GraphQL、tRPC)が必要です。これにより、ルート登録、シリアライゼーションコントラクト、および「UIコード」と「APIコード」間のコンテキストスイッチが導入されます。

制約: Reactの"use server"ディレクティブは、関数レベルでシリアライゼーション境界を定義します。ランタイムは、これらのアノテーションされた関数をインターセプトし、HTTPエンドポイントとして公開し、引数/戻り値のシリアライゼーションを透過的に処理する必要があります。このメカニズムは、プログレッシブエンハンスメントのためにクライアントサイドJavaScriptなしでも動作する必要があります。

決定: サーバー関数をプライマリミューテーションインターフェースとしてサポートします。アノテーションされた関数はクライアントコンポーネントおよびプレーンHTMLフォームから呼び出し可能です。別のAPIルートレイヤーは不要です。

export default function App() { async function addItem(formData) { "use server"; await db.insert({ name: formData.get("name") }); } return ( <form action={addItem}> <input name="name" /> <button type="submit">Add</button> </form> ); }

トレードオフ:

結果: ミューテーションロジックはレンダリングコードとコロケーションされ、別のAPIルートレイヤーが排除されますが、UIとサーバーロジック間の結合が増加します。フォームベースの送信はクライアントサイドJavaScriptなしで動作しますが、より豊かなインタラクションパターンのコストが伴います。TypeScriptの型はシリアライゼーション境界を越え、静的型チェックを提供しますが、ランタイムバリデーションはありません — 開発者が明示的なチェックを追加しない限り、不正なペイロードは拒否されません。

これらの決定は、実行レイヤーの上でアプリケーションを構築・合成する方法を形成します:コアの拡張方法、ルートの定義方法、コンテンツの記述方法、利用可能な出力モード、およびキャッシュとプログレッシブエンハンスメントがレンダリングパイプラインとどのように統合されるかです。

問題: すべての機能(ルーティング、MDX、静的エクスポートなど)をコアにバンドルするモノリシックなランタイムは、シンプルなユースケースには膨大であり、開発者がカスタマイズできる範囲を制限します。

制約: プラグイン境界は推論可能なほど狭くなければなりませんが、ファーストパーティ機能(例:ファイルシステムルーティング)をコアロジックではなくプラグインとして実装できるほど表現力がある必要があります。Viteのプラグインインターフェースとの互換性が必要であり、独自の拡張モデルは避けるべきです。

決定: コアを最小限に保ちます。ファイルシステムルーターを含む高レベル機能をプラグインとして実装します。Viteのプラグインコントラクトに沿った設定とライフサイクルフックを公開します。

トレードオフ:

結果: ルーティングMDXを使用しないアプリケーションは、それらの機能のためのコードや設定のコストを支払いません。新しい機能はコアを変更することなくプラグインとして追加できますが、これにより統合の複雑さがプラグイン作者に移ります。プラグインインターフェースがViteネイティブであるため、アダプターレイヤーは不要です — ただし、プラグインの動作はViteのプラグイン順序とライフサイクルセマンティクスに従います。

問題: プログラマティックなルート設定は、アプリケーションの成長とともにメンテナンスの負担になります。ルート定義が実際のファイル構造から乖離し、パスのリファクタリングにはファイルと設定の両方の更新が必要です。

制約: ルーティングはオプショナルでなければなりません — カスタムエントリーポイントを使用するアプリケーションは規約に強制されるべきではありません。ルーティングが使用される場合、RSCレンダリングパイプラインと統合し、レイアウト、ネストされたルート、ルートレベルでの非同期データロードをサポートする必要があります。

決定: ファイルシステムベースのルーティングをオプトインプラグインとして実装します。ディレクトリとファイルの命名規約がルートセグメントにマッピングされます。カスタムルーティングソリューションは、直接エントリーポイント使用により引き続きサポートされます。

pages/ index.tsx → / about.tsx → /about (root).layout.tsx → layout wrapper auth/ login.tsx → /auth/login posts/ [id].tsx → /posts/:id

トレードオフ:

結果: ルート構造はディレクトリレイアウトによって決定され、別のルート設定ファイルの必要性がなくなりますが、ファイルシステムにルーティングセマンティクスが埋め込まれます。ルートの変更はファイルのリネームまたは移動であり、リファクタリングが簡素化されますが、ファイルシステムがルーティング規約に準拠する必要があります。カスタムエントリーポイントパスはフォールバックとして引き続き利用可能ですが、ファイルシステムルーティングからプログラマティックルーティングへの移行にはプロジェクトの再構築が必要です。

問題: ドキュメントサイトやコンテンツの多いページをJSXコンポーネントとして作成するのは面倒です。より軽量な記述フォーマットが必要ですが、インタラクティブなReactコンポーネントの埋め込みも可能でなければなりません。

制約: コンテンツページはRSCレンダリングパイプラインに参加するためにサーバーコンポーネントである必要があります。Markdown/MDX処理はビルド時またはサーバー上で行われ、クライアントでは行われません。RemarkおよびRehypeプラグインの互換性が期待されます。

決定: ページソースとしてMarkdownおよびMDXをサポートします。MDXファイルはサーバーコンポーネントにコンパイルされ、標準のimport/export構文を介してクライアントコンポーネントを埋め込めます。RemarkおよびRehypeプラグインチェーンは設定可能です。

import Counter from "./Counter"; # My Page Here is an interactive counter embedded in MDX: <Counter />

トレードオフ:

結果: コンテンツの記述はJSXではなくMarkdown構文を使用し、非コンポーネントコンテンツの障壁を下げますが、混在コンテンツでMDX固有のエッジケースが導入されます。RemarkおよびRehypeプラグインはアダプターなしでビルドパイプラインに組み込み可能ですが、プラグインの互換性は固定されたMDXコンパイラバージョンに依存します。

問題: すべてのページが実行中のサーバーを必要とするわけではありません。マーケティングページ、ドキュメント、その他の静的コンテンツは、ビルド時にHTMLにプリレンダリングすることで恩恵を受けます。ただし、純粋な静的システムは動的ページを処理できません。

制約: 静的エクスポートは、HTML(初期ロード用)とRSCフライトペイロード(クライアントサイドナビゲーション用)の両方を生成する必要があります。同じコンポーネントツリーが、コード変更なしで静的モードとサーバーモードの両方でレンダリング可能でなければなりません。

決定: エクスポートステップとして静的生成をサポートします。ページは個別に静的エクスポート用にマークできます。出力にはHTMLとRSCペイロードの両方が含まれます。エクスポートされたファイルは任意の静的ファイルサーバーまたはランタイム自体で配信可能です。

// about.static.ts — /aboutを静的生成用にマーク export default true; // posts/[id].static.ts — 動的ルートのパラメータを列挙 export default [{ id: "1" }, { id: "2" }, { id: "3" }];

トレードオフ:

結果: 静的ページはランタイムを起動せずに配信され、それらのルートのサーバーコンピュートが排除されますが、コンテンツの更新にはリビルドが必要です。静的ページ間のクライアントサイドナビゲーションはプリレンダリングされたフライトペイロードを使用し、エクスポートされたページ数に比例してビルド出力サイズが増加します。ハイブリッドデプロイメント(一部静的、一部動的)には静的ファイルホストと実行中のサーバープロセスの両方が必要であり、デプロイメントトポロジーの複雑さが増します。

問題: JavaScriptに依存するアプリケーションは、JavaScriptが無効なユーザー、制限されたネットワーク上のユーザー、または主に初期HTMLとやり取りする支援技術を使用するユーザーにとってアクセスしにくくなります。

制約: サーバー関数はクライアントサイドJavaScriptなしで標準HTTP POSTを介したフォーム送信を受け付ける必要があります。ナビゲーションは<a>タグで動作する必要があります。アーキテクチャはJavaScriptが利用可能な場合にリッチなインタラクティビティをサポートしながら、グレースフルに劣化する必要があります。

決定: フォームはサーバー関数エンドポイントに直接送信されます。リンクはフルページサーバーレンダリングをトリガーします。クライアントサイドJavaScriptは、ロードされるとクライアントサイドトランジションとストリーミング更新でフォームとナビゲーションをプログレッシブに拡張します。

トレードオフ:

結果: JavaScript無しのパスは機能的ですが劣化した体験を生成します — ナビゲーションはフルページロードをトリガーし、フォームインタラクションにはクライアントサイドのフィードバックがありません。JavaScriptが利用可能な場合、クライアントコンポーネントがハイドレートしベースラインを拡張しますが、これにより独立してテストする必要がある2つの異なる動作パスが作成されます。ESMを介した遅延ロードクライアントコンポーネントはコンポーネント境界ごとにJavaScriptペイロードを分割しますが、バンドルされない限り各追加クライアントコンポーネントがネットワークリクエストを追加します。

問題: 命令的キャッシュAPI(useCache()useResponseCache())は、開発者がキャッシュ可能な関数やルートハンドラーを手動でラップすることを要求します。これによりキャッシュロジックがコードベース全体に散在し、どの関数がどのTTLとタグでキャッシュされているかを監査することが困難になります。

制約: キャッシュディレクティブは宣言的で、適用する関数とコロケーションされている必要があります。キャッシュキーはビルド間で安定している必要があります — コード変更はキャッシュを暗黙的に無効化するべきです。キャッシュバックエンドはプラガブル(メモリ、localStorage、sessionStorage、Unstorageドライバーを介した外部ストア)である必要があります。サーバーとクライアントコンポーネントの両方にキャッシュサポートが必要ですが、異なるストレージバックエンドを使用します。

決定: インライン設定構文を持つ"use cache"ディレクティブをサポートします:"use cache: <provider>; ttl=<ms>; tags=<tag1>,<tag2>; profile=<name>"

async function getTodos() { "use cache; ttl=200; tags=todos"; const res = await fetch("https://jsonplaceholder.typicode.com/todos"); return res.json(); }

Viteプラグインがコンパイル時に関数本体を書き換え、ファイルパス、ソースコンテンツ、AST位置のMD5ハッシュをキャッシュキーとしてuseCache()でラップします。ロックマップが同時キャッシュミスのサンダリングハード問題を防ぎます。組み込みプロバイダーにはメモリ、サーバー(プリレンダリング対応)、クライアント(ブラウザメモリ)、localStorage、sessionStorage、nullが含まれます。カスタムプロバイダーは設定可能です。

トレードオフ:

結果: キャッシュは関数レベルでプロバイダー、TTL、タグを1行で宣言されます。キャッシュキーは自動的に安定し、無効化に安全です。ただし、文字列DSLは静的解析ツールに対して不透明であり、二重レイヤーキャッシュは非自明な相互作用効果を生み、プラガブルプロバイダーシステムはディレクティブ自体を超えた設定知識を必要とします。

問題: すべてのリクエストに対して同一のRSC出力を再レンダリングすると、サーバーリソースが浪費されます。一般的なページ(ランディングページ、商品リスト)はユーザー間で同一であることが多く、キャッシュから配信できます。

制約: キャッシュはオプトインで、レスポンスごとに制御可能でなければなりません。キャッシュキー、TTL、再バリデーション戦略は開発者定義でなければなりません。キャッシュレイヤーは特定のインフラストラクチャ(Redis、CDNなど)を前提としてはいけません。

決定: 明示的なキャッシュ制御APIを備えたインメモリレスポンスキャッシュを提供します。開発者はルートまたはレスポンスごとにキャッシュディレクティブを設定します。キャッシュ実装は置換可能です。

トレードオフ:

結果: キャッシュされたページは再レンダリングなしで配信されます。再バリデーションにより動的データの結果整合性が可能になります。プラガブルキャッシュインターフェースにより、ランタイムの変更なしでRedis、Memcached、またはCDNレベルのキャッシュとの統合が可能です。

これらの決定は、本番環境、開発中、およびデプロイメントターゲット全体でシステムがどのように動作するかを管理します:プラットフォーム固有のビルドアダプター、マルチプロセススケーリング、開発フィードバックループ、およびHTTPリクエスト/レスポンスを超えてサーバーの機能を拡張するプロトコル統合です。

問題: デプロイメントプラットフォーム(VercelNetlifyCloudflareBunDeno)はそれぞれ異なる出力構造、エントリーポイント形式、ランタイムAPIを必要とします。Node.jsサーバーバンドルを生成するビルドは、大幅な再構築なしにCloudflare Workersにデプロイできません。

制約: アダプターは、ランタイムの内部ビルド出力(サーバー、クライアント、静的、RSCファイルを含む.react-server/ディレクトリ)を、アプリケーションコードを変更することなくプラットフォームの期待する出力に変換する必要があります。アダプターはPPR対応ルーティング(静的配信から.postponed.jsonファイルを除外)、依存関係トレーシング(Node.jsサーバーレスバンドル用)、エッジ対Node.jsエントリーポイントの選択を処理する必要があります。

決定: パイプラインを標準化するcreateAdapter()ファクトリを提供します:出力をクリア → ファイルをコピー/分類 → アダプター固有のハンドラーを呼び出し → オプションでデプロイ。各アダプターは出力ディレクトリ構造、エントリーポイント(エッジ:@lazarv/react-server/edge、Node:@lazarv/react-server/node)、およびプラットフォーム固有の設定生成(Vercel Build Output API、wrangler.tomlNetlify _redirects)を指定します。Node.jsアダプターは依存関係トレーシングに@vercel/nftを使用し、エッジアダプターはすべてを単一チャンクにバンドルします。

トレードオフ:

結果: 単一の--adapter <name>フラグでプラットフォーム固有のデプロイメント出力が生成されます。アプリケーションコードはターゲット間で変更されません。ただし、エッジデプロイメントはフルランタイムの制限されたサブセットであり、依存関係トレーシングはNode.jsターゲットのビルド時間を追加し、各プラットフォームの独特な出力形式には専用のアダプターメンテナンスが必要です。

問題: 単一のNode.jsプロセスは1つのCPUコアを使用します。マルチコアマシンでの本番デプロイメントはハードウェアを十分に活用できず、単一のブロックされたイベントループはすべての同時リクエストのスループットを低下させます。

制約: Node.jsクラスタリングはスレッドベースではなくプロセスベースです。各ワーカーは独自のメモリ空間を持つ独立したプロセスです。共有状態(インメモリキャッシュ、WebSocket接続)はワーカー間で自動的に伝播しません。

決定: 本番ビルドでNode.jsクラスターモードをサポートします。ランタイムはデフォルトでCPUコアごとに1つのワーカーを生成します。各ワーカーは独立してリクエストを処理します。

# 利用可能なすべてのCPUコアで起動 REACT_SERVER_CLUSTER=on pnpm react-server start # またはワーカー数を指定 REACT_SERVER_CLUSTER=8 pnpm react-server start

トレードオフ:

結果: スループットは利用可能なCPUコアに応じてスケールします。ブロックまたはクラッシュしたワーカーがプロセス全体をダウンさせることはありません。並行処理モデルはアプリケーションコードに対して透過的です — クラスターモードで実行するためにコード変更は不要です。

注意: クラスターモードはNode.jsの本番ビルドでのみ利用可能です。

問題: コード変更時のフルページリロードは開発フローを中断し、クライアントサイドの状態を破棄します。RSCはサーバーコンポーネントをサーバー上で再レンダリングし、更新されたフライトペイロードを配信する必要があるため、複雑さが追加されます。

制約: HMRはクライアントとサーバーの両方のコンポーネントをカバーする必要があります。ViteのHMRプロトコルはクライアントモジュールをネイティブに処理しますが、サーバーコンポーネントの変更にはサーバーサイドの再レンダリングとRSC出力の差分が必要です。

決定: クライアントコンポーネントにはViteのHMRインフラストラクチャを活用します。サーバーコンポーネントではファイル変更時にサーバーサイドの再レンダリングをトリガーし、更新されたRSCペイロードをクライアントにストリーミングすることで拡張します。

トレードオフ:

結果: サーバーコンポーネントのHMRレイテンシーは、インプレースモジュールスワップではなくサーバーサイドの再レンダリングとフライトペイロードの差分が必要なため、クライアントのみのコンポーネントよりも高くなります。影響を受けないサブツリーのクライアントサイド状態はサーバーコンポーネントHMR間で保持されますが、再レンダリング境界内のコンポーネントの状態はリセットされます。開発時のモジュール変換の動作は、本番のみのバンドリングの問題を再現しない可能性があります。

問題: AIエージェントは標準化されたプロトコルを通じてサーバーサイドのツールを発見し呼び出す必要があります。MCP(Model Context Protocol)サーバーの構築には、通常、独自のHTTP処理、スキーマバリデーション、トランスポート管理を持つ別のサービスが必要です — アプリケーションサーバーが既に提供しているインフラストラクチャの重複です。

制約: MCPツールは、既存のアプリケーションコードベース内でサーバー関数として定義可能でなければならず、同じモジュールシステムとデプロイメントパイプラインを使用します。MCPトランスポートはステートレスなエッジデプロイメントとの互換性のためにストリーマブルHTTP(WebSocketのみではない)でなければなりません。ツール入力スキーマはランタイムでバリデーションされる必要があります。

決定: ランタイムのエクスポートとしてcreateTool()createResource()createPrompt()、およびcreateServer()プリミティブを提供します。MCPツールは入力バリデーション用のZodスキーマを持つ"use server"モジュールで定義されます。

"use server"; import { createTool } from "@lazarv/react-server/mcp"; import { z } from "zod"; export const echo = createTool({ id: "echo", title: "Echo", description: "Echoes the input back", inputSchema: { input: z.string() }, async handler({ input }) { return `Echo: ${input}`; }, });

createServer()@modelcontextprotocol/sdkからMcpServerをインスタンス化し、すべてのツールを登録し、MCPストリーミングトランスポートをReadableStreamボディを持つWeb Responseにブリッジする非同期HTTPハンドラーを返します。

トレードオフ:

結果: MCPサーバーは、おなじみのサーバー関数パターンを使用してアプリケーションコードベース内で定義可能です。AIエージェントは標準化されたMCPプロトコルを介してツールを発見します。ただし、ステートレスなトランスポートは会話型ワークフローを制限し、Zodの依存関係はツール作者にとって非オプショナルであり、MCPエンドポイントはアプリケーションサーバーのリソース制約を共有します。