15 ポイント 投稿者 GN⁺ 2025-06-02 | 1件のコメント | WhatsAppで共有
  • プログレッシブJPEGのように、JSONデータも不完全な状態で先に送信し、クライアントが徐々に内容全体を活用できる方式を紹介
  • 既存のJSONパース方式には、データ全体を完全に受信するまで何も処理できないという非効率性の問題がある
  • Breadth-first方式でデータを複数のチャンク(部分)に分け、まだ準備できていない部分はPromiseで表し、準備できしだい段階的に埋めていくことで、クライアントは未完成のデータも活用できる
  • この概念は**React Server Components(RSC)**の中核的な革新であり、<Suspense>を通じて意図した段階的なローディング状態を制御する
  • データストリーミングと意図的なUIローディングフローを分離することで、より柔軟なユーザー体験を提供できる

プログレッシブJPEGとProgressive JSONのアイデア

  • プログレッシブJPEGは、画像を上から下へ一度に読み込む代わりに、まず全体をぼんやり表示し、徐々に鮮明になる方式
  • これと同様に、JSON転送にもプログレッシブ方式を適用することで、全体が完成するまで待たずに一部のデータを即座に使うことができる
  • 例となるJSONデータ構造では、通常の方式では最後のバイトまで受信しないとパースできない
  • そのためクライアントは、サーバーの遅い部分(例: 遅いDBからcommentsを読み込む処理)まですべて送信されるのを待つ必要があり、これは現在の標準として非常に非効率である

ストリーミングJSONパーサーの限界

  • ストリーミングJSONパーサーを導入すると、不完全な(途中段階の)データオブジェクトツリーを生成できる
  • しかし、各オブジェクトのフィールド(例: footer、複数のcomment一覧など)が一部しか届いていない場合、型が一致せず、完成済みかどうかも把握しづらいため、活用しにくいという問題が生じる
  • HTMLのストリーミングレンダリングと同様に、順番どおりにストリーム処理すると、ひとつの遅い部分が全体の結果を遅らせるという問題も同じように起こる
  • これが一般にストリーミングJSONがあまり使われない理由である

Progressive JSON構造の提案

  • 従来の深さ優先ストリーミング(つまり、ツリー構造の下位まで内部をたどって送信する方式)ではなく、Breadth-first(幅優先)方式を導入
  • 最上位オブジェクトだけを先に送信し、下位の値はPromiseのようなプレースホルダーとして置いておき、準備できしだいそれぞれを個別のチャンクとして埋めていく
  • たとえばサーバーは、非同期のデータ読み込みが完了するたびに対応するチャンクを送信し、クライアントは準備できた分だけ利用できる
  • **非同期なデータ受信(早期ロード)**が可能になり、複数の遅い部分の処理がすべて終わるまで全体を待つ必要がなくなる
  • クライアントをチャンク単位の非順次受信および部分的な順次受信に強く設計すれば、サーバーはさまざまなチャンク分割戦略を柔軟に適用できる

InliningとOutlining: 効率的なデータ転送

  • プログレッシブJSONストリーミング形式では、再利用されるオブジェクト(例: 同じuserInfoを複数箇所から参照)も重複保存せず、ひとつのチャンクとして切り出して各場所から同じ参照を共有できる
  • 遅い部分だけを分離してプレースホルダーとして送り、それ以外はすぐ埋めることで効率的なデータストリームを実現する
  • 同じオブジェクトが何度も現れる場合でも、一度だけ送信して再利用(Outlining)できる
  • この方式により、**循環参照(オブジェクトが自分自身を参照する構造)**も通常のJSONのように扱いに困らず、チャンク間の間接参照構造として自然にシリアライズできる

React Server Components(RSC)のプログレッシブストリーミング実装

  • 実際のReact Server Componentsは、プログレッシブJSONストリーミングモデルを適用した代表例である
    • サーバーは外部データ(例: Post、Comments)を非同期に読み込む構造を使う
    • クライアントでは**まだ到着していない部分をPromiseとして扱い、**準備できた順にUIを段階的にレンダリングする
  • Reactの<Suspense>で意図的なローディング状態を設定
    • ユーザー体験上不要な画面のジャンプを防ぐため、Promise状態(穴)をそのまま見せるのではなく、<Suspense>のfallbackで段階的なローディング演出が可能
    • データが素早く到着しても、実際のUIは設計された段階に合わせて段階的に表示されるよう、開発者が制御できる

要約と示唆

  • React Server Componentsの中核的な革新は、コンポーネントツリーのpropsを外側から段階的にストリーミングする方式にある
  • したがって、サーバーがすべてのデータを完全に準備するまで待つ必要はなく、主要な部分から少しずつ見せながら、ローディング待機状態も細かく制御できる
  • ストリーミングそのものだけでなく、それを活用するプログラミングモデル(Reactの<Suspense>)のような構造的支援もあわせて必要である
  • これにより、遅いデータの一部分が全体を遅らせるといった従来の転送方式のボトルネックを緩和できる

1件のコメント

 
GN⁺ 2025-06-02
Hacker Newsの意見
  • この投稿をあまりに文字どおりに受け取っている人がいるようだが、Dan Abramov が Progressive JSON という新しいフォーマットを提案しているわけではない、という説明
    • この投稿は、React Server Components のアイデアと、コンポーネントツリーを JavaScript オブジェクトの形で表現し、それをストリームとして送る過程を説明する内容
    • この方式では React コンポーネントツリーに「穴」を設けられるため、最初はローディング状態やスケルトン UI を見せ、サーバーから実データを受け取ったときにその部分を完全にレンダリングする構造が可能
    • これにより、より細かな粒度でローディング表示と高速な初期表示が可能になる
  • 個人的には、人々がこのアイデアを拡張して別の方法に応用するのもよいと思う
    • RSC のデータをシリアライズする方法を React に限定せず、より一般的なパターンとして説明しようとした意図
    • React Server Components で見つかったさまざまなアイデアが、他の技術やエコシステムにも取り入れられてほしいという願い
  • 私は progressive loading の方式、特にコンテンツがずっと動く(ジャンプする)体験はあまり好きではない
    • ロード中に空っぽの状態 UI を見せるパターンが特に気になる
  • 少し前まで Ember を使っていたときにも似たような方式があり、Ajax エンドポイントを書くのがとてもつらかった記憶
    • ツリー構造を再配置して一部の子要素がファイル末尾に来るようにし、DAG(非巡回グラフ)の処理を効率化しようという意図だったようだ
    • SAX スタイルのストリーミングパーサーを使えば、データが部分的に届いた時点で先に描画を始められる
    • ただし単一スレッド VM で処理順序を誤って設計すると、かえって問題が大きくなる危険がある
  • 私はすでに AI ツールとの連携で、ストリーミング partial JSON(Progressive JSON)方式を実際に使っている
    • この方式は RSC だけでなくさまざまな場面で活用でき、実務的にもクライアント・サーバー双方に価値があるという実体験
  • Dan の "2 computers" 発表と最近の RSC 関連投稿を一通り見た
    • Dan は React エコシステムで最高の説明者だが、技術をここまで難しく説明しなければならないなら
      1. 本当に不要な技術か
      2. 抽象化に問題があるか
    • 大多数のフロントエンド開発者はいまだに RSC の概念を完全には理解していない
    • Vercel は Next.js を事実上の標準 React フレームワークにし、RSC の採用もその流れに乗って広がった
    • Next.js を使う人でさえ Server Component の境界を明確に理解しておらず、一種の「カーゴカルト」的な採用が多い
    • React が Vite 関連の PR を受け入れなかったのも疑わしい。RSC の推進は実際にはユーザーや開発者のためではなく、プラットフォーム企業のホスティング販売戦略なのではないかという考え
    • 振り返れば、Vercel が React のオリジナルチームを大量に採用したのも、React の未来を主導しようという意図に見える
    • 歴史的な動機や背景に対する見立てが間違っているという指摘とともに、Vite サポートの現状についての説明
    • Vite 統合は DEV 環境でバンドリングが必要という技術的制約のため、現在 Vite チームが改善を進めているという言及
    • 人々が RSC を理解していないという論点は、論理的に循環した主張だという見方
    • RSC が嫌いでもよいが、その中には他の技術に応用できる興味深いアイデアが十分にある
    • 説得するというより、不思議で有用な部分をそれぞれが持ち帰ってほしいという願い
  • もちろん今でも SPA を静的サイトとして作って CDN に載せることはできるし、Next.js も「dynamic」モードでセルフホスト可能
    • ただし Next.js のサーバーレスレンダリング機能全体を Vercel 以外で完全に実装するのは難しいという現実がある(文書化されていない “magic” のため)
    • この問題についても、マルチプラットフォームで一貫した API 提供のためにアダプター導入を公式提案している: https://github.com/vercel/next.js/discussions/77740
    • 私は RSC 推進が企業利益のためというより、従来の Web サイト構築パターン(SSR + クライアントで少しの progressive enhancement)に実際かなり多くの利点があると気づいたことに由来する、という立場
    • SSR だけでも、ビジネスロジックが不必要にクライアント側へ多く移ってしまう問題がある
  • RSC 自体は興味深い技術だが、実戦ではそれほど合理的ではないという考え
    • 複雑なコンポーネントをレンダリングするために Node/Bun バックエンドサーバーを大規模に維持する負担がある
    • むしろ静的ページや React SPA + Go API サーバーの組み合わせのほうがずっと効率的
    • 似た結果をはるかに少ないリソースで作れる
  • 新しい技術の説明が複雑だからといって、必ずしも不要な技術や誤った抽象化だと断定することはできず、複雑さを受け入れる価値のある問題もある
    • 今後この技術がどう進化するか見守るという観点
  • RSC のコード構造を活用して、小さな断片に HTML/CSS/JS を分ける静的ページビルドも可能ではないかという考え
    • 投稿で提案された「$1」プレースホルダーを URI に置き換えてもサーバーが不要な場合がある(ほとんどの動的 SSR が必須なわけではない)
    • 欠点は、この方式ではコンテンツ変更時の更新パイプライン、特に S3 にコンパイル済み静的サイトをストリーミング配信する場合の速度確保が重要になること
    • たとえば多くの記事が事前レンダリングされている新聞サイトのように、コンテンツの一部だけが変わるときにその部分だけ効率よく再ビルドするなど、賢いコンテンツ diff 処理が必要
  • 実務ではパフォーマンス最適化と言いながら、フロントエンドで数 MB のデータをミリ秒単位で読み込み複雑なロジックを処理する一方、実際には BFF やアーキテクチャ改善、より Lean な API 構築のほうがはるかに生産的な解決策だと気づく
    • GraphQL、http2 などによる試みもあったが、結局は本質的な問題解決ではなく、Web 標準の進化なしにはパラダイム変化はないだろうという意見
    • 新しいフレームワークでもこの限界は同じ
  • RSC は投稿の終盤で説明されているように、本質的には BFF(Backend for Frontend)の役割だという説明
  • 「ページ読み込み ms 短縮」の意味次第だという意見
    • Time to first render、time to visually complete を最適化するなら、空の skeleton UI を先に送り API でデータを受け取って hydrate する方式が体感上もっとも速い
    • 逆に time to first input、time to interactive を速くしたいなら、ユーザーデータをその場でレンダリングできる必要があり、この場合はバックエンドのほうがはるかに有利(ネットワーク呼び出し最小化)
    • ほとんどの場合、ユーザーはこのほうを好み、CRUD SaaS アプリではサーバーサイドレンダリングが、Figma のようにデザインが重要なアプリではクライアント側で静的データ + 追加データ取得が適している
    • 「あらゆる問題に効く一つの解決策」はなく、最適化ポイントは主観的な選択
    • 開発体験、チーム構成など技術選定に影響するさまざまな要素がある
  • おかげで、なぜ Facebook の読み込み時にコアコンテンツがいつも最後にレンダリングされるのか理解できた
  • ここで言う BFF が何なのか気になるという質問
  • 略語が多すぎて FE と BFF が何なのか気になるという反応
  • Progressive JSON のアイデアを自分で使いたいとは思わず、代替案はいくつもあると思う
    • 最も簡単な解決策は、巨大な 1 つの JSON オブジェクトを複数に分割し、つまり「JSON lines」として送ること
    • ヘッダー情報は 1 回だけ送り、巨大な配列は 1 行ずつ送ってストリーム処理を効率化する
    • オブジェクトがさらに大きければこの方式を再帰的に適用することもできるが、複雑になりすぎるかもしれない
    • サーバーがプロパティ順序を明示的に保証すれば、progressive parsing や分離も可能
    • 結局、本当に巨大な構造には向かないかもしれないが、よくある大規模 JSON を扱う場面ではかなり実用的な道具
  • 明示的に穴(holes)を示さなくても、ストリーミングメッセージを順次送りながら差分(diff)だけを送る方式が可能
    • 「Mendoza」という差分フォーマットを使えば、Go、JS/Typescript で非常にコンパクトにパッチ(diffs)を送れる: https://github.com/sanity-io/mendoza, https://github.com/sanity-io/mendoza-js
    • zstd のバイナリ diff 方式や Mendoza のように、シリアライズデータの一部だけをメモリに保持して効率的にパッチを進める
    • React でも差分比較や変更点だけの注入が必要なので、意味のあるアプローチ
  • UI データストリーミングでは空配列や null だけでは不十分で、現在どのデータが未到着(pending)状態なのかという別情報が必要
    • GraphQL のストリーミングペイロードは、有効なデータスキーマと未到着情報、そして後続パッチ処理を組み合わせる方式を選んでいる
  • どの部分が「穴」なのか分かる必要があるので、ローディング状態を見せやすい
  • クライアントで JSON を progressively decode するなら、jsonriver というライブラリの紹介: https://github.com/rictic/jsonriver
    • 非常にシンプルな API で、性能も良くテストも十分
    • ストリーミングされた文字列断片を、徐々に完成度の高い値としてパースしてくれる
    • 最終結果が JSON.parse と同一であることを保証
  • ツリーデータならどんな構造にも適用できる面白い方式だという意見
    • ツリーデータは parent、type、data ベクトルと string table で表現すれば、残りはすべて少数の整数に縮約できる
    • string table と type info をヘッダーとして upfront 送信し、parent、data ベクトルのチャンクをノード単位でストリーミング
    • depth-first/breadth-first ストリーミングはチャンク順を変えるだけで十分
    • ネットワーク上のアプリでロード時間 UX を大きく改善できそう
    • テーブルとノードチャンクを交互に送って、ツリーをどんな順序でも Web 上に可視化できる
    • preorder traversal と depth 情報だけあれば node id なしでもツリー構造を復元可能
    • このアイデアで小さなライブラリを作るのも価値ある試み
  • ほとんどのアプリにこうした「精巧な」ローディングシステムは不要で、もっと単純に複数回の API 呼び出しで代替できる場合が大半だという主張
    • 自分は RSC wire protocol の動作方式を説明したかっただけで、実際にこういうものを自作しろと勧める意図はない、という反論
    • さまざまなツール間の原理を理解することは、結局いろいろな場所でアイデアを流用したりリミックスしたりする助けになる
    • 複数回呼び出す戦略には n*n+1 問題があると思うが、OOP/ORM スタイルでオブジェクトをネストして送る代わりに、コメントのようにフラットに送る方法もある
    • そうするくらいなら、むしろ protobuf など型が明確なエンドポイント構成にも利点がある
    • comments を分離すれば 2 回の呼び出しで十分処理できる(ページ+本文、コメントは別)。こうすれば pre-render 最適化も可能
    • 選択肢の実装複雑度そのものを上げすぎず、あらかじめ定義された良いツールがあるなら、わざわざ深くカスタマイズする必要はない
    • 過度に複雑な機能は、結局ユーザーや開発者にとって毒になり得ることを認識すべきという立場
    • 640K で十分だという話のように、progressive/partial reads は WASM 時代の UX 速度に確かに大きな役割を果たし得ると思う
    • protobuf のようなバイナリエンコーディング方式に partial read と well-defined streaming が加われば、エンジニアの負担は増えても結果の UX は大きく向上する可能性
  • Progressive JPEG はメディアファイルの性質上必要だが、Text/HTML では別に必要なく、JS バンドルが大きくなった結果として複雑性だけが増す自己矛盾的な状況だという見解
    • 実際の遅さの原因が単にデータの「サイズ」だけではないことを指摘
    • サーバーデータのクエリ自体に時間がかかったり、ネットワークが遅かったりするときにも、段階的な表示(progressive reveal)には意味がある
    • 全データがそろうまで待つ代わりに、適切なタイミングでローディング UI を見せる意図的な段階的レンダリングは、実際にユーザー体験の向上につながる
  • エンドポイント分離戦略は、すでにさまざまな利点(Head of line blocking の回避、フィルタオプション改善、ライブ更新、独立した性能改善など)を備えた解決策だという考え
    • アプリケーションを document platform として扱おうとする試み自体が根本問題だという立場
    • 実際のアプリケーションは「文書」のようには動作せず、このギャップを埋めるために多くの追加コードやインフラが必要になっている状況
    • 別エンドポイント採用の本当の欠点と進化の方向については、次の 2 本の長文で補足説明している: https://overreacted.io/one-roundtrip-per-navigation/, https://overreacted.io/jsx-over-the-wire/
    • 要するに、エンドポイントは結局サーバー/クライアント間の「公式な」API 契約となり、コードがモジュール化されるにつれて性能面で不利(ウォーターフォール現象など)が生じうる
    • サーバー側で判断を一括処理すること(coalescing)が、性能と構造的柔軟性の両面でより良い代案になり得る