Web Streams APIは、ストリーム(逐次的なデータの塊)を扱うための標準的なインターフェイスです。大容量のデータを一度に全て読み込まず、データをチャンク(小塊)に分割して順次処理することで効率的に扱えます (Using web streams on Node.js)。Webブラウザで策定された仕様ですが、現在ではブラウザ、Node.js、Denoなど主要なJavaScript環境でサポートされています (Node.js Streams vs. Web Streams API | Better Stack Community)。
● 基本概念(ReadableStream・WritableStream・TransformStream):
Web Streams APIには3種類の主要なストリーム型があります (Using web streams on Node.js):
- ReadableStream – データのソースを表す「読み取り可能な」ストリームです。たとえばネットワークから届くデータやファイルの内容などをチャンク単位で読み取ることができます(ストリームの利用側のコードをコンシューマと呼びます) (Using web streams on Node.js)。
- WritableStream – データの送信先を表す「書き込み可能な」ストリームです。ここにデータを書き込むことで、ファイルへの保存やネットワーク送信などを逐次的に行えます(ストリームへデータを書き込むコードをプロデューサと呼びます) (Using web streams on Node.js)。
-
TransformStream – 入力用のWritableStreamと出力用のReadableStreamのペアから構成されるストリームです。データの変換処理を行う中間ストリームとして機能し、ReadableStreamの
pipeThrough()
メソッドでパイプラインに挿入できます (TransformStream - Web APIs | MDN)。例えば、動画フレームのデコード/エンコード、データ圧縮/展開、XMLからJSONへの変換といった処理を実装できます (TransformStream - Web APIs | MDN)。Web標準でいくつかのTransformStream実装が提供されており、例えば**TextDecoderStream
(UTF-8バイト列を文字列に変換)やCompressionStream
**(バイナリデータをGZIP圧縮)などがあります (Using web streams on Node.js)。
● ストリームのパイプライン処理:
Streams APIでは、ReadableStreamからWritableStreamへパイプ(接続)してデータを逐次流すことができます。たとえば、readableStream.pipeTo(writableStream)
を呼ぶと、ReadableStreamから読み取ったデータを直接WritableStreamへ送り込むことが可能です (ReadableStream - Web APIs | MDN)。また、readableStream.pipeThrough(transformStream)
を使うと途中にTransformStreamを挟んでデータ変換しつつパイプできます (ReadableStream - Web APIs | MDN)。これらのメソッドはバックプレッシャーも管理しており、送り先のWritable側が一杯の場合はReadable側の読み出し速度を自動的に調整してくれます。
● Streams APIの利用例:
-
Fetch APIでの活用: Fetch APIはネットワークレスポンスを表す
Response
オブジェクトのbody
プロパティにReadableStreamを提供しています (ReadableStream - Web APIs | MDN)。つまり、fetch()
した結果をストリームとして読み込むことが可能です。例えばHTTPレスポンスが巨大なJSONや動画ファイルでも、全体をメモリに置かずチャンクごとに処理できます。以下はFetch APIのレスポンスボディを逐次読み取るコード例です(テキストデータを扱うためTextDecoderStream
でUTF-8デコードしています):
// サーバーからテキストデータをストリーミング取得する例
const response = await fetch('https://example.com/large-text');
const textStream = response.body.pipeThrough(new TextDecoderStream()); // バイト→文字列に変換
for await (const chunk of textStream) {
console.log('受信チャンク文字列:', chunk);
// 逐次チャンクデータを処理(表示や解析など)
}
console.log('ストリームの読み取り完了');
上記のようにfor await...of
ループを用いると、FetchレスポンスのReadableStreamから非同期イテレーションで簡潔にチャンクを取得できます (ReadableStream - Web APIs | MDN)。Fetchが返すReadableStreamはUint8Array
(バイト列)として各チャンクを提供しますが、TextDecoderStream
を挟むことで文字列に変換しつつ処理しています。
-
Node.jsでの活用: Node.jsでは従来から
stream
モジュールによるストリームAPIが存在しましたが、現在はWeb Streams APIにも対応しつつあります (Node.js Streams vs. Web Streams API | Better Stack Community)。たとえばNode.js v18以降では実験的にfetch
やReadableStream
がグローバルで利用でき、レスポンスのbody
はWeb StreamsのReadableStreamとして取得できます。Node.js環境でWeb Streamsを扱う際には、必要に応じてNode.js独自のストリームと相互変換も可能です。例として、Nodeの組み込みユーティリティstream
にはReadable.fromWeb()
というメソッドがあり、Web StreamsのReadableStreamをNode.jsのReadableストリームに変換できます (Readable Streams in Node.js)。これによりサーバサイドでもWeb Streams APIを使ったパイプライン処理(例えばファイル->ネットワーク応答へのストリーミング送信など)が行えます。Node.jsとWeb Streams APIの具体的な違いや使い分けについては、環境固有の最適化(ファイルシステムアクセスはNode Streams、Web標準処理はWeb Streamsなど)を考慮して選択するとよいでしょう。
React Concurrent Streaming (RCS) の概要と仕組み
React 18以降では、Concurrent Rendering(コンカレントレンダリング)と呼ばれる新しいレンダリング方式や、これを活かしたストリーミングSSR(サーバサイドレンダリング)の強化が導入されています。RCSとは、これらReactの並行レンダリング機能を利用したストリーミング描画全般を指す概念で、特にサーバコンポーネント(React Server Components)を含めた最新のSSR手法を含んでいます。
● ReactのConcurrent Renderingの基礎:
従来のReact(同期レンダリング)では、状態更新やレンダリング処理がブロッキング的に実行され、重い処理があるとUI更新が滞ることがありました。React 18のConcurrent Modeでは、内部的にレンダリングを分割し優先度付けする仕組みが導入され、処理を中断・再開したり、急ぎの更新を先に行ったりといった柔軟なスケジューリングが可能になっています。これによりブラウザのメインスレッドを占有する時間が短縮され、ユーザ入力への応答性が向上します。
SSR(サーバサイドレンダリング)の場合、Concurrent Renderingの概念は主にストリーミング出力の文脈で活用されます。Reactはレンダリング結果(HTML文字列など)を逐次生成し、準備ができた部分から順次クライアントに送信できます。これを支えるのがReact 18の非同期レンダリングとSuspenseの組み合わせです。
● Streaming SSR(ストリーミングSSR)の仕組み:
React 18以前にもSSRでストリーム出力するrenderToNodeStream()
が存在しましたが、Suspenseと連携してデータの到着を待つような動作はできず、結果的にすべてのHTMLを生成し終わるまでクライアントに送れないケースがありました (A guide to streaming SSR with React 18 - LogRocket Blog)。React 18ではSSRが大幅に刷新され、Suspenseを用いた遅延コンテンツの部分出力が可能になりました。
具体的には、React要素ツリー中に<Suspense fallback={<Loading/>}>
を配置すると、サーバ側レンダリング時にそのboundary(境界)内部のコンテンツが未解決(データ待ち)の場合でも、外側のHTMLを先にクライアントへ送信できます。React 18の新しいサーバレンダリング関数は、まず**「シェル」(Shell)と呼ばれる即座にレンダリング可能な部分**を出力し、残りの非同期部分は準備でき次第、後からストリームに流し込みます (A guide to streaming SSR with React 18 - LogRocket Blog) (A guide to streaming SSR with React 18 - LogRocket Blog)。
例えば以下のようなReactコンポーネントを考えます:
<App>
<Header /> {/* すぐレンダリング可能 */}
<Suspense fallback={<Spinner/>}>
<Article /> {/* データ取得が必要 */}
</Suspense>
<Footer /> {/* すぐレンダリング可能 */}
</App>
React 18のSSRでは、<Header>
と<Footer>
はすぐにレンダリングされ、<Spinner>
(フォールバックUI)も含めた初期HTMLがまずクライアントに送られます。一方、<Article>
内で例えばデータフェッチが行われている場合、その完了を待ってから本来の内容が後続のHTMLストリームとして送信され、クライアント側でそれまで表示していた<Spinner>
と差し替わります。こうした逐次的・非ブロッキングなHTML送信により、SSRでありながらクライアントはできる限り早く部分的にレンダリング結果を受け取れるようになります (A guide to streaming SSR with React 18 - LogRocket Blog)。
この仕組みを実現するために、React 18ではサーバ側APIとしてReactDOMServer.renderToPipeableStream()
およびrenderToReadableStream()
が導入されました(後述)。これらはConcurrentなストリーミングSSRを行うための関数で、Suspenseやコード分割(React.lazy
)と深く統合されています (A guide to streaming SSR with React 18 - LogRocket Blog)。その結果、HTMLおよびデータフェッチのウォーターフォール問題(逐次待ちによる遅延)を解消し、データ要件に応じてアプリケーションをインクリメンタル(段階的)にレンダリング・送信できるようになっています (A guide to streaming SSR with React 18 - LogRocket Blog)。
● React 18以降の新機能(Suspense・Server Componentsなど):
React 18では上記のSuspenseによるストリーミングSSRに加え、将来的なフル機能のReact Server Components (RSC) の土台となる機能も導入されています。React Server Componentsとは、一部のコンポーネントをサーバ上でレンダリングし、その結果(UIの記述データ)をクライアントへストリーム送信して統合する仕組みです。RSCはReact 18ではまだ実験的機能ですが、Reactチームが提案する「ゼロバンドルサイズ」でのサーバ側UIロジック実行を可能にします。RSC用のプロトコルは**“React Flight”**と呼ばれ、テキストベースのストリームでコンポーネントツリーを表現します。クライアント側ではそれをパースして通常のReactコンポーネントに戻す必要があります ([React Server Components, without a framework?
](https://timtech.blog/posts/react-server-components-rsc-no-framework/#:~:text=%23%23%20%60createFromReadableStream%60%20from%20%60react))。
React公式のRSCデモ実装では、react-server-dom-webpack
パッケージを利用しており、クライアントにはcreateFromReadableStream()
という関数が提供されています。これはサーバから送られてきたReadableStreamを受け取り、ReactのFlightプロトコルメッセージをパースして「サーバコンポーネントのツリー」を再構築するラッパー関数です ([React Server Components, without a framework?
](https://timtech.blog/posts/react-server-components-rsc-no-framework/#:~:text=%23%23%20%60createFromReadableStream%60%20from%20%60react))。言い換えれば、`createFromReadableStream`はReactのFlightクライアントAPIのラッパーであり、サーバコンポーネントの受信処理を簡素化します ([React Server Components, without a framework?
](https://timtech.blog/posts/react-server-components-rsc-no-framework/#:~:text=%23%23%20%60createFromReadableStream%60%20from%20%60react))。React 18ではこの他にも、(実験段階のものも含め)`React.use()`フックによるサーバからの値読み取りや、`SuspenseList`コンポーネント、`useId`による一意ID生成などSSRを支える様々な新機能が追加されています。
まとめると、React Concurrent Streaming (RCS)とはReactのコンカレント機能(非同期レンダリング・Suspense等)を駆使してSSRをストリーミング実行する一連の技術を指し、必要に応じてServer Componentsのストリーミングも含めた最先端のSSR手法です。次節では、このRCSを実現する上でWeb Streams APIが果たす役割について詳しく見ていきます。
Web Streams APIとReact Concurrent Streamingの関係性
ReactのストリーミングSSRは、本質的には「サーバで生成されるHTML出力を逐次クライアントに送り出す」ことです。この逐次送信を可能にするのがストリームであり、React 18では実行環境に応じてNode.jsのストリームまたはWeb Streams APIのReadableStreamを利用します。すなわち、Web Streams APIはReactのストリーミングSSRの裏側でHTMLやコンポーネントデータを転送するための基盤として機能します。
● Web Streams APIを用いたストリーミングSSRの実装方法:
React 18のSSR関数には、Node.js向けのrenderToPipeableStream
と、ウェブ環境向けのrenderToReadableStream
が用意されています。どちらもReact要素ツリーをストリームにレンダリングするAPIですが、返却するストリームの型が異なります。Node.js(サーバランタイム)ではNode.jsのReadableストリームが返され、Web Streams対応ランタイム(DenoやCloudflare Workersなど)ではWeb Streams APIのReadableStreamが返されます (renderToPipeableStream – React)。React公式ドキュメントでも「このAPIはNode.js専用。DenoやエッジランタイムのようにWeb Streamsが使える環境ではrenderToReadableStream
を使うべき」と明記されています (renderToPipeableStream – React)。
実装の観点では、Node.jsサーバでのSSRではrenderToPipeableStream()
を呼び出して得られたストリームをHTTPレスポンスにパイプし、クライアントへ送信します。一方、Deno DeployやCloudflare Workersのような環境では、renderToReadableStream()
で得たReadableStreamをそのままResponse
オブジェクトに渡して返す、という形になります。どちらの場合もReactはバックグラウンドで適切にSuspense境界を判断し、準備のできたHTMLチャンクを順次ストリームに流すため、開発者はストリームをパイプ接続するだけでストリーミングSSRを実現できます。
● renderToPipeableStream
と renderToReadableStream
の違い:
前述のとおり、両者の違いは主に利用環境と返り値の型にあります。renderToPipeableStream
はNode.js専用であり、戻り値としてstream.Readable
(Nodeのストリーム)を提供します (A guide to streaming SSR with React 18 - LogRocket Blog)。一方、renderToReadableStream
はブラウザの標準ReadableStream
を返す関数で、DenoやEdge Runtime用に設計されています (What's New in React 18 - Medium)。機能的にはどちらもSuspense対応のストリーミングSSRを実現しますが、例えばNode.jsのHTTPレスポンスオブジェクト(http.ServerResponse
)にはNodeストリームをそのままpipe()
できるためNode版APIを使い、Service WorkerのFetchイベントではResponse
を直接返す必要があるためWeb Streams版APIを使う、という形で使い分けます。
内部実装では、この2つのAPIはReactドメインの共通ロジックを共有しており、出力先インターフェースだけを切り替えていると考えてよいでしょう。したがってストリーミングされる内容(HTML構造や順序)は同一ですが、Node版は.pipe(res)
でHTTP送信し、Web Streams版は例えばreturn new Response(stream)
のように使われます。React 18の発表ブログでも、「モダンなエッジ環境向けにrenderToReadableStream
を追加した」と紹介されています (What's New in React 18 - Medium)。
● フロントエンドでのデータ受信・処理:
ストリーミングSSRで送られてきたデータをフロントエンドでどのように処理するかは、送られてくる内容によって異なります。
-
通常のHTMLストリーミングの場合: サーバから送信されるのはプレーンなHTML文書です。ブラウザはHTTPチャンクを受け取るごとにHTMLを順次パース・レンダリングするため、特別な受信処理をしなくてもユーザはコンテンツが徐々に表示されるのを確認できます。開発者側では、通常SSRと同様に
ReactDOM.hydrateRoot()
(React 18以降はhydrateRoot
)を使って既存のサーバレンダリング済みDOMにイベントを紐付け、Reactによるインタラクティブな状態にします。なお、React 18のhydrateRoot
は**部分的なHydration(選択的ハイドレーション)**にも対応しており、ストリーミング中でも利用可能です (A guide to streaming SSR with React 18 - LogRocket Blog)。つまり、サーバから送られてきたHTMLの一部だけがまだプレースホルダ(例えば<div>Loading...</div>
のようなSuspenseフォールバック)の状態でも、クライアントはその部分を飛ばして先に他の要素をHydrationし、後から差し込まれた本来のコンテンツを検知して自動的にHydrationを完了する仕組みになっています。これにより、ストリーミングSSRでもクライアント上での初期化が順調に進み、インタラクション可能な部分から先に有効化されていきます (A guide to streaming SSR with React 18 - LogRocket Blog)。 -
Server Components (RSC) のストリーミング場合: サーバコンポーネントの仕組みでは、サーバから送られてくるのはHTMLではなく特殊なシリアライズ済みのコンポーネントデータです(Reactの用語で「Flight Payload」と呼ばれるものです)。このデータはJSONに似たテキストストリームで、クライアント側でReact専用のデコーダによってパースされ、実際のReactコンポーネントツリーに復元されます。具体的には、先述の
createFromReadableStream
関数(react-server-dom-webpack
パッケージのAPI)がそれを担います ([React Server Components, without a framework?- クライアントで
fetch('/endpoint/that/returns/rsc')
を呼び出し、サーバコンポーネントのReadableStreamレスポンスを取得する。 - そのReadableStreamを
ReactFlightClient.createFromReadableStream(...)
(Flightプロトコルのクライアント実装)に渡す。 - 戻り値として**Thenable(Promiseのようなもの)**が得られるので、それを使用してサーバコンポーネントのツリーを取得する。React 18.3以降では
React.use()
フックと組み合わせ、Suspenseで囲むことで非同期に読み込まれたコンポーネントを表示することができます (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back)。
簡単な例を示します。サーバが
/react-payload
というエンドポイントでサーバコンポーネント<Profile server={true}>
の結果をストリームとして返すとします。クライアント側コードは以下のようになります:import { createFromReadableStream } from 'react-server-dom-webpack/client'; import { Suspense } from 'react'; // ... const response = await fetch('/react-payload?user=123'); const stream = response.body; const serverTreePromise = createFromReadableStream(stream); // Suspenseを使ってサーバコンポーネントを非同期にレンダリング function ProfilePage() { return ( <Suspense fallback={<div>Loading profile...</div>}> {/* use()フックでThenableを読み込む (React 18.3+ 仮想コード) */} {React.use(serverTreePromise)} </Suspense> ); } ReactDOM.createRoot(document.getElementById('root')).render(<ProfilePage />);
上記のコードでは、
createFromReadableStream(response.body)
がサーバからのストリームを受け取り、Thenableなコンポーネントツリーを返す部分です。この値をReact 18.3以降で利用可能なReact.use()
(現時点では試験的)の中で使用することで、サーバコンポーネントが解決されるまでSuspenseのfallbackを表示し、解決後に自動でコンテンツが差し込まれます (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back)。なお、React.use()
が使えない場合はserverTreePromise.then(tree => {...})
で通常のPromiseとして扱い、解決後に明示的にレンダーする方法もあります。 - クライアントで
要するに、Web Streams APIのReadableStreamは、React Concurrent Streamingにおいてサーバ→クライアント間でUI情報を運ぶパイプとなっています。React 18のSSR機能は、そのパイプを通じてHTMLやコンポーネントデータを逐次送り出し、クライアントではそれを逐次受け取ってレンダリング・ハイドレーションすることで、高速かつスムーズなユーザ体験を実現します。
具体的なコード例
ここでは、Web Streams APIとReact Concurrent Streamingを組み合わせた実装を、具体的なコード例で示します。Express.jsによるSSRストリーミングの例、Fetch APIでのデータストリーミングの例、そしてServer ComponentsとWeb Streams APIを併用したデモの順に紹介します。
Express.jsを使ったStreaming SSRの実装
まず、Node.js上のExpressサーバでReact 18のストリーミングSSRを行うコード例です。renderToPipeableStream
を利用し、生成されたNode.jsストリームをHTTPレスポンスにパイプしてクライアントへ送ります(Reactアプリ内でSuspenseを使っていれば自動的にストリーミング挙動になります)。
import express from 'express';
import ReactDOMServer from 'react-dom/server';
import App from './App'; // サーバサイドレンダリングするReactアプリ
const server = express();
server.get('*', (req, res) => {
const { pipe } = ReactDOMServer.renderToPipeableStream(<App />, {
onShellReady() {
// エラーフラグに応じてステータス設定(略)
res.status(200).setHeader('Content-Type', 'text/html');
// Reactから返されたストリームをHTTPレスポンスにパイプ
pipe(res);
},
onError(err) {
console.error('SSR Error:', err);
}
});
});
server.listen(3000);
上記のコードでは、renderToPipeableStream(<App />, {...})
を呼び出すと、即座にpipe
関数(Nodeストリームをパイプするための関数)とabort
関数等が得られます。onShellReady
コールバックは、「シェル」(最初に送信可能なHTML部分)が準備できたタイミングで呼ばれます (A guide to streaming SSR with React 18 - LogRocket Blog)。ここでHTTPヘッダをセットし、pipe(res)
でレスポンスストリームに流し込みを開始します (A guide to streaming SSR with React 18 - LogRocket Blog)(なお、bootstrapScripts
オプションでクライアントのJSを先行読み込みさせることもできます (A guide to streaming SSR with React 18 - LogRocket Blog))。こうすることで、<App />
の内容はクライアントへストリーミング送信されます。<App />
内部でSuspenseによる遅延コンテンツがある場合、ReactはonShellReady
時点で用意できたHTMLだけをまず送信し、残りは裏で準備が整い次第ストリーム経由で追加送信します (A guide to streaming SSR with React 18 - LogRocket Blog) (A guide to streaming SSR with React 18 - LogRocket Blog)。
従来のrenderToString
やrenderToNodeStream
と異なり、renderToPipeableStream
はHTMLを最後までバッファしないため、サーバ上で非同期データ待ちがあってもその部分を除いた出力を先に送れます (A guide to streaming SSR with React 18 - LogRocket Blog)。以上のコードにより、ExpressサーバでのストリーミングSSRが実現できます。
Fetch APIのReadableStreamを利用したデータストリーミング
次に、クライアント側JavaScriptでFetch APIのReadableStreamを扱う例を示します。ここではサーバからイベントデータを逐次受け取って表示するようなシナリオを仮定します。サーバはtext/event-stream
などで逐次メッセージ(例: JSON文字列)を送り、クライアントでそれをストリーム処理します。
// クライアント側: Fetchでサーバからストリームレスポンスを取得
const response = await fetch('/api/stream-events');
if (!response.ok || !response.body) {
throw new Error('ストリーム取得失敗');
}
const stream = response.body.pipeThrough(new TextDecoderStream()); // バイトストリーム -> テキストストリーム変換
const reader = stream.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
console.log('受信イベントデータ:', value);
// ここで必要に応じてJSONパース: JSON.parse(value) など
}
console.log('すべてのイベント受信完了');
このコードでは、fetch
で取得したレスポンスのbody
からReadableStreamを取得し、TextDecoderStream
でテキストに変換しつつgetReader()
でリーダーを取得しています。reader.read()
を繰り返し呼ぶことでサーバから送られてきたチャンクを1つずつ取り出しています (ReadableStream - Web APIs | MDN) (ReadableStream - Web APIs | MDN)。done
がtrue
になればストリームの終端です。ここでは単にコンソールに出力していますが、実際には各value
(テキストデータ)をJSONデコードして画面に追加描画する、といった処理が行えます。
上記のようにFetch APIのReadableStreamを使うことで、クライアントアプリケーションでも逐次データ処理が可能になります。例えばチャットアプリで新着メッセージをストリーミング受信したり、プログレスバー付きで大きなファイルをダウンロードしたりといった実装がシンプルに書けます。Streams APIにより、ネットワーク越しのデータもイベント駆動的に少しずつ処理でき、ユーザにすぐフィードバックを返すことができます。
Server ComponentsとWeb Streams APIを組み合わせたデモ
最後に、ReactのServer Components (RSC) とWeb Streams APIを組み合わせて利用する高度なデモンストレーションです。ここではサーバコンポーネントの結果を初期HTMLと一緒にストリーミングし、クライアントで復元する手法の概略を示します。
サーバ側: Reactの実験的なRSCレンダラー(react-server-dom-webpack
等)を用いてサーバコンポーネントをストリームとして生成し、それを通常のHTMLストリームに埋め込みます (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客)。具体的には次のような処理手順になります:
-
サーバコンポーネントのレンダリング – 例えば
renderToReadableStream(<App />)
を呼び出し、サーバコンポーネント<App />
のFlightデータストリーム(ReadableStream)を取得します (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back)。このストリームにはUIツリーがテキストデータとして逐次書き出されてきます。 -
HTMLストリームへの統合 – 続いて、上記のFlightストリームを二手に分岐します(
stream.tee()
を使用 (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back))。一方のストリームはそのまま保持し、もう一方は特殊な<script>
タグを差し込むTransformStreamを通してHTML出力に注入します (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客)。このTransformStream(例えばinjectRSCPayload
という実装)は、HTMLの適切な箇所にシリアライズ済みコンポーネントデータを埋め込む役割を果たします (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客)。最後に通常のReactDOMServer.renderToReadableStream(<Content/>)
を実行し、上記TransformStreamで処理することでRSCデータが埋め込まれたHTMLストリームを得ます (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back) (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back)。 -
クライアントへの送信 – 完成したHTMLストリーム(ReadableStream)をHTTPレスポンスとしてクライアントに送ります。HTML中には埋め込み用の複数の
<script>
タグが挿入されており、RSCのペイロードが分割格納されています (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back)。
クライアント側: ブラウザはまず送られてきたHTMLを解析・表示します。埋め込みの<script>
タグには通常実行されないタイプ(例えばtype="x-component"
など)でRSCペイロードが入っているとします。クライアントのJSはそれらを検出して内容を取得・連結し、もとのReadableStreamに復元します (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客)。rsc-html-stream
というライブラリでは、これを行うrscStream
という仮想的なReadableStreamが提供されており、サーバで埋め込んだのと逆順にHTMLから取り出したデータをReadableStreamとして再構成します (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back)。最後に、その再構成されたRSCストリームをReactServerDOMReader.createFromReadableStream()
に渡すことで、元の<App />
コンポーネントツリーがクライアント上に復元されます (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客)。復元後は通常のReactコンポーネントと同様にReactDOM.hydrateRoot
等でHydrationしてインタラクティブにします。
以上の流れにより、追加のHTTPリクエストなしにサーバコンポーネントのデータを初期レンダリングに織り交ぜることができます (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客) (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客)。この手法では、従来はクライアントが別途fetchで取得していたサーバコンポーネント情報を、SSRのHTML内に直接バンドルするため初回ロードが高速化します (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客)。一方で実装は高度であり、先述のrsc-html-stream
のような支援ライブラリが登場しています。Reactの公式ソリューションとしてはNext.js 13以降で同様のアプローチ(RSCのインライン化)が採用されており、開発者が意識せずともこの仕組みが内部で動くようになっています。
上記デモはかなり発展的な内容ですが、Server ComponentsのストリーミングとWeb Streams APIの強力な組み合わせを示す好例です。サーバ・クライアント間でUIを構成するデータをストリーム送受信し、途切れなくユーザにページを提供できる点で、Web Streams APIはReactの最新SSR技術を支える不可欠なパーツとなっています。
応用事例と最適化
最後に、Web Streams APIとReact Concurrent Streamingを活用した応用シーンやパフォーマンス最適化について解説します。
● SSRのパフォーマンス最適化:
ストリーミングSSRは、伝統的なSSRに比べて大きなパフォーマンス上の利点があります。最大の利点はTTFB(Time To First Byte)の短縮です。レンダリング開始後、最初のバイトがクライアントに届くまでの時間が大幅に早くなり、ページサイズにかかわらず一定して速い傾向があります (Streaming Server-Side Rendering)。React 18のストリーミングでは、ページ全体がレンダリング完了するのを待たずに送信を開始できるため、ユーザは非常に早い段階で何らかのコンテンツを目にできます (Streaming Server-Side Rendering) (Mastering Server-Side Rendering (SSR) in React 19 with Vite: The Ultimate Guide for Developers - DEV Community)。特にSuspenseを駆使することで、重要なAbove-the-fold(ファーストビュー)コンテンツを即座に表示しつつ、重たい部分は後から読み込み・描画するといった戦略が可能です。これによりユーザビリティが向上し、体感速度の大幅な改善につながります (Mastering Server-Side Rendering (SSR) in React 19 with Vite: The Ultimate Guide for Developers - DEV Community)。実際、「ユーザがコンテンツを目にするまでの時間が短縮され、たとえ低速回線でも早期にページ構造が見えてくる」という報告があります (Mastering Server-Side Rendering (SSR) in React 19 with Vite: The Ultimate Guide for Developers - DEV Community)。さらに、React 18のSSRは複数のデータソースの待ち合わせ(ウォーターフォール現象)を解消するため、あるコンポーネントのデータ待ちで他の部分のレンダリングが遅れることがありません (A guide to streaming SSR with React 18 - LogRocket Blog)。結果としてサーバの処理・ネットワーク送信・クライアント描画が並行的に進み、全体として効率が上がります。
● Web Streams APIを活用した効率的なデータ転送:
Web Streams API自体もアプリケーションの効率を高めます。ストリーム処理ではデータをチャンクに分割して逐次処理するため、巨大なデータセットでもメモリ消費を抑えつつ処理できます (Node.js Streams vs. Web Streams API | Better Stack Community)。例えば数GBに及ぶファイルを読み書きする場合でも、ストリームを使えば数KB~数MB単位で分割して処理するため、サーバ・クライアント双方でメモリフットプリントを低く保てます (Node.js Streams vs. Web Streams API | Better Stack Community)。また、ネットワーク伝送においてもストリームは遅延を隠蔽し、パイプライン上で処理を重ねることで帯域を有効活用します。送信側ではデータを送り終える前に次のデータ生成に取りかかれ、受信側では到着した分からすぐパースやレンダリングにかかれます。たとえば圧縮ストリーム(CompressionStream)を使えばサーバでオンザフライ圧縮しつつ送信し、クライアントで逐次展開しながら処理する、といったことも可能で、これにより通信量削減とリアルタイム処理を両立できます。さらにWeb Streams APIのバックプレッシャー制御によって、受信側が詰まっているときは送信側が待機するため、無駄なデータ再送やバッファ溢れを防ぐことができます。総じて、Web Streams APIを適切に用いることで、データ転送のスループット向上とリソース効率化が図れます。
● React Suspenseとの組み合わせによるUX向上:
Suspenseは、ストリーミングSSRやサーバコンポーネントと組み合わせることで強力なUX改善手段となります。従来、SSRでは全データが揃うまで完全なUIを出せないため、ユーザは白紙のページかローディングインジケータだけを見せられて待たされるケースがありました。Suspenseを用いたストリーミングでは、ロード中の部分にフォールバックUI(ローディングスピナー等)を表示しつつ、他の部分は即座に本物のコンテンツを表示できます。これによりユーザには「ページの一部は表示されており、残りも読み込み中である」ことが視覚的に伝わり、心理的な待ち時間の負担が軽減します。実際、React 18のHTMLストリーミングはSuspense境界ごとにチャンクを送るため、画面が段階的に構成されていく体験を提供できます (Enhancing SSR with React 18: A deep dive into server side rendering optimisation | Equal Experts)。また、クライアント側のSelective Hydrationと組み合わせれば、ユーザがすぐ操作したいUI(例えばメニューや記事一覧)は即座にインタラクティブになり、読み込み中の部分(詳細記事の本文など)は後からインタラクティブになる、といった段階的エンハンスメントも可能です。これは大きなUX向上ポイントで、重いページでもまず骨格をすぐ使える状態にし、徐々に肉付けするような動的振る舞いが実現します。
以上、Web Streams APIとReact Concurrent Streaming (RCS) の関係性について、概要から具体例・応用まで詳しく解説しました。Web Streams APIは高速・効率的なデータ逐次処理の基盤を提供し、ReactのConcurrent Streamingはそれを活かしてユーザ体験を向上させるSSR技術です。両者を適切に組み合わせることで、現代的なウェブアプリケーションにおいてスピーディーかつリソースフレンドリーなサーバレンダリングを実現できるでしょう。その結果、ユーザはより快適にコンテンツへアクセスでき、開発者はパフォーマンスと開発容易性を両立させることができます。
参考文献・資料: Web Streams API(MDN) (ReadableStream - Web APIs | MDN) (TransformStream - Web APIs | MDN)、React 18 ストリーミングSSR公式ブログ (A guide to streaming SSR with React 18 - LogRocket Blog) (A guide to streaming SSR with React 18 - LogRocket Blog)、LogRocket解説記事 (A guide to streaming SSR with React 18 - LogRocket Blog) (A guide to streaming SSR with React 18 - LogRocket Blog)、その他関連ドキュメント・記事 (renderToPipeableStream – React) (探索未来前端渲染新纪元:rsc-html-stream详解与应用推广-CSDN博客) (GitHub - devongovett/rsc-html-stream: Inject an RSC payload into an HTML stream and read it back)など。