世界を動かす技術を、日本語で。

プログレッシブJSON

概要

  • Progressive JPEG の考え方をデータ転送に応用する提案
  • JSONの従来転送 では、全データ到着まで利用不可
  • ストリーミングJSON は不完全なデータ構造の扱いが課題
  • Progressive JSON はプレースホルダーとPromiseで部分的にデータ利用可能
  • React Server Components はこの手法をUIストリーミングに応用

Progressive JPEGの発想とJSON転送への応用

  • Progressive JPEG は画像を上から順にではなく、最初はぼやけて徐々に鮮明に表示する方式
  • この考え方を JSONデータ転送 に応用、部分的にデータを受信・利用可能にする発想
  • 従来のJSON転送は 全データ受信完了 までクライアント側で何もできない
  • サーバー側で一部の生成が遅い場合、全体のレスポンスが遅延する問題

ストリーミングJSONの課題

  • ストリーミングJSONパーサ を使うと、未完のデータツリーを途中まで構築可能
  • 例えば、コメントが全て揃っていない場合、配列の一部しか取得できない状態
  • オブジェクトに 必須プロパティが欠落 しているため、型の整合性が取れない
  • どのデータが 完全か未完か不明 で、アプリケーションロジックで扱いが難しい
  • このため、ストリーミングJSONは一部のニッチな用途以外では普及していない

順序通りのストリーミングの限界

  • HTMLのストリーミング も同様に、ドキュメント順に配信
  • 上部だけ表示され、下部(例:footer)は生成済みでも遅延して表示
  • データ生成の遅い部分 が全体の表示を妨げる構造的欠点
  • 一部が遅いと 後続の全データが遅延 する問題

Progressive JSON:幅優先のストリーミング

  • 幅優先(breadth-first) でデータを送信、未送信部分は プレースホルダー("$1"など) で表現
  • プレースホルダーは後から Promise として解決される
  • クライアント側では 未解決部分をPromise として保持し、利用可能な部分から処理開始
  • 例えば、headerやfooterだけ先に表示、postやcommentsは後から順次解決

効率化のためのインライン化

  • 全てを細かく分割しすぎると逆に効率低下
  • 遅延が発生する箇所のみ をプレースホルダー化し、他はまとめて送信
  • クライアントは 受信順が前後しても問題ない ように設計
  • サーバーは バッチングやチャンク化の戦略 を柔軟に選択可能

アウトライン化と重複排除

  • 同一オブジェクトが複数回出現する場合、 一度だけ送信し、参照で再利用
  • 例:userInfoオブジェクトを"$1"として複数箇所で使い回す
  • 2回以上使われた時点で インライン→アウトライン に切り替え可能
  • 循環参照を持つ サイクリックオブジェクト にも対応可能

ストリーミングデータとストリーミングUI

  • React Server Components はこのProgressive JSONの考え方をUIに応用
  • サーバーで非同期にデータを生成し、 Promiseとして部分的にクライアントへ配信
  • クライアント側では 未解決部分はPromise として保持、解決次第UIを更新
  • 例:PostやCommentsのデータが遅延しても、headerやfooterは即表示
  • ただし、UIの「穴」を避けるため、Reactは <Suspense> でローディング状態を明示的に制御

まとめ

  • Progressive JPEG の発想をデータやUIストリーミングに応用することで、 部分的な表示や処理が可能
  • 幅優先のデータ配信Promiseによる非同期解決重複排除 などのテクニックで効率化
  • Reactのようなモダンフレームワークでは、 Progressive JSON 的なストリーミングが標準化されつつある
  • クライアントは 部分的なデータ受信 でも柔軟に処理可能な設計が求められる

Hackerたちの意見

すごくいいポイントだね。これは一般的にどんなツリーデータにも当てはまるよ。ツリーデータは親、タイプ、データベクターを使って表現するのが好きで、文字列テーブルも使うから、他は小さい整数だけなんだ。文字列テーブルとタイプ情報を最初にヘッダーとして送って、その後に親とデータベクターのチャンクをNノードずつまとめて送る感じ。深さ優先や幅優先のストリーミングはベクターの順序を選ぶことになるね。これ、ちょっと試してみなきゃ!ネットワークに依存するアプリケーションで、もっとサクサクなロードタイムUXを実現する一般的な方法になるかも。

... 小さなライブラリを作る価値があるかもしれないね。

テーブルとノードのチャンクを交互に送ることもできるよ!これで、親を先に見せることなく子供を先に見せたり、任意のグラフを表現したりできるんだ!面白いアプリケーションにつながるかも。

もし事前順序でツリーを送るなら、ノードIDや親IDなしでツリーを送れるよ!各ノードのレベルだけ送れば、スタックを使ってツリー構造を復元できるんだ。

99.9999%* のアプリは、これほど「ファンシー」なものは必要ないよ。幅優先が重要なら、複数回呼び出せばいいだけだし(やり方によってはオーバーヘッドもほとんどない)。 * これは自分で考えた数字だけど、現状は「正しい」ってことで。

実際には640K以上は必要なかったよね。プログレッシブまたは部分的な読み込みがあれば、アプリケーションが劇的に速くなると思う。特にフロントエンドでWASMの時代に入ると、部分読み込みに対応したprotobufのような適切なバイナリエンコード形式があれば素晴らしい。エンジニアにはもっと仕事が増えるけど、UXの改善は大きいかも。

はっきり言っておくけど、これをアプリに手動で実装することは勧めないよ。ただ、RSCのワイヤプロトコルがどう機能するかをざっくり説明してるだけ。物語的には「基本からの発明」みたいにまとめてるけど、読んでて楽しいからね。RSCを使うことを無理に勧めるつもりもないけど、ツールの設計がどうなってるかを理解するのは便利だと思う。時には、いろんなツールからアイデアを取り入れてリミックスすることもあるし。

「偶然に過剰設計」するのは、実際に良いオプションがあるって意味では悪くないよね。でも、オフ・ザ・シェルフのオプションに「ファンシー」な機能を追加するのは問題がある。もしその「ファンシー」な機能が、実際には「複雑なエンジニアリングの問題」で、使うときに実際のメカニズムを考慮していない人をつまずかせるようなものだったらね。

ここにいる人たちの中には、著者(ダン・アブラモフ)が「プログレッシブJSON」というフォーマットを提案していると勘違いしている人もいるみたいだけど、そうじゃないよ。これはReactサーバーコンポーネントのアイデアを説明する投稿で、コンポーネントツリーをJavaScriptオブジェクトとして表現し、それをブログ投稿に似たフォーマットでストリーミングするって内容なんだ(似た機能はあるけど、バンドラーやフレームワークに依存してると思う)。これにより、Reactはツリーに穴(読み込み状態を表す)を持たせて、最初の読み込み時にフォールバック状態を表示し、その後サーバーがデータを提供できるときにのみ読み込まれたコンポーネントツリーを表示できるようになるんだ(つまり、フォールバックスピナーやスケルトンをもっと早く表示できるし、細かい読み込みができる)。 (このコメントは細かいところで間違ってるかもしれないけど、主なアイデアは合ってると思う。)

そうだね!正直言うと、みんながこのアイデアを使って別のことをするのも全然気にしないよ。RSCのデータシリアライゼーションの考え方を、あまりReact特有に見えないように説明したかったんだ。実際にはもっと一般的なアイデアだからね。RSCで見たアイデアが他の技術にも広がるといいな。

もう実際にAIツールコールでストリーミング部分的なJSONレスポンス(プログレッシブJSON)を使ってるよ。RSCを超えて、これが普通になってきてるし、クライアントとサーバーをじっくり見れば実用的な使い方がたくさんあるよ。

プログレッシブローディングが嫌いな人って私だけですか?特にコンテンツが動き回る場合は。最もイライラするアンチパターンは、読み込み中に空の状態のUIを表示することです。

プログレッシブJPEGは理にかなってるよね。メディアファイルだから、そもそも大きいし。テキストやHTMLはそうでもないけど。なんか、自分たちで複雑にしてる感じがする。JSバンドルが巨大で、今度はストリーミングでさらに複雑にしてるっていう。

物事が遅くなるのは、大きいからじゃなくて、生成や受信に遅延がかかるからなんだよね。遅延はサーバー側にあることもあって、実際にクエリに時間がかかるものもあるし、キャッシュするのが難しいこともある。ユーザーのネットワーク環境が悪いせいで遅延が出ることもあるし。どちらの場合でも、全体を待つんじゃなくて、コンテンツが利用可能になるにつれて段階的に表示するのが良いっていう利点があるよ(意図的なローディングステージを使って)。

パフォーマンスに関して見たことがあるのは、ページの読み込みをミリ秒単位で短縮しようとして、何MBも取得してフロントエンドで複雑な処理をしてる人たち。実際にはBFFを書くことや、アーキテクチャを改善したり、よりスリムなAPIを作る方が生産的なんだけどね。GraphQLやHTTP2でそれを試みたけど、正直言って失敗したと思う。ウェブ標準がちゃんと進化しない限り、根本的な問題は解決できないよ。新しいフレームワークもそれを解決するわけじゃないし。

この文脈でのBFFって何?最近はAIの親友を書くのも珍しくないけど…。

少なくともこの投稿は、Facebookのページを読み込むときに本当に重要なのは(コンテンツ)最後に読み込まれるってことを説明してくれてるね。

「ページの読み込み時間を短縮する」というのは、何を意味するかによりますよね。初回レンダリングの時間や視覚的に完了する時間を最適化するなら、できるだけ少ないロジックでページをレンダリングする必要があります。空のスケルトンを送って、そこにユーザーデータをAPI経由で流し込むのが、ユーザーの読み込み速度の感覚には最も早いです。初回入力時間やインタラクティブになる時間を短縮したいなら、実際にユーザーデータを使って動作するページを構築する必要があります。これはしばしばバックエンドで行うのが最も早く、ネットワークコールを減らせるからです。ほとんどのユーザーは実際にはそれを好むと思いますが、アプリによりますね。CRUDのSAASアプリのようなものはサーバーサイドでレンダリングするのが一番良いでしょうが、Figmaのようなものはもっと静的なページを送ってから、フロントエンドでユーザーのデザインデータを取得するのがベストです。すべてに通用する解決策があるという考えは間違っています。なぜなら、最適化するものは主観的な選択だからです。それに、Dev体験やチームのトポロジー、コンウェイの法則など、技術選択に大きな影響を与える要素もありますから。

ダンの「2台のコンピュータ」についての話を見たり、RSCやその利点についての最近の投稿を読んだりしました。ダンはReactエコシステムの中で最高の説明者の一人ですが、個人的には、こんなに苦労して技術を売り込んだり説明したりしなきゃならないなら、2つの可能性があると思います。1つ目は、その技術に本当の必要性がないこと。2つ目は、抽象化が欠陥だということ。#2はちょっと当たってる気がします。私が知っているほとんどのフロントエンド開発者はRSCをまだ「理解」していないですし。Vercelはこれをユーザーに強力に推進していて、RSCの普及はNext.jsがデフォルトのReactフレームワークとして浮上したおかげです。Next.jsのユーザーの中でも、ほとんどの開発者はサーバーコンポーネントの境界を本当に理解していないようで、ただ流行に乗っているだけです。それに、ReactがViteを使ってReactアプリを作る方法を言及したPRをマージすらしないのを見ると、RSCの推進が本当にユーザーや開発者のためなのか、それともベンダーが自社のホスティングプラットフォームを押し出すためのものなのか疑問に思います。もしS3からCDNでフロントエンドしたSPAを簡単に出せるなら、VercelやNetlifyにとってはあまり良くないですよね。振り返ってみると、VercelがOG Reactチームのメンバーをたくさん雇ったのは、Reactの未来をコントロールするための手段だったんじゃないかと思います。単なるタレント獲得ではなくて。

あなたの分析はとても良いと思いますし、VercelがRSCを強力に推進している理由にも同意します。

RSCのコード構造を使って、HTML、CSS、JSの小さなチャンクに分けた静的ページをコンパイルする世界があると思う。要するに、記事の"$1"プレースホルダーをURIに置き換えれば、サーバーは必要なくなるよ。(ほとんどの場合、完全に動的なSSRは要らない)大きなデメリットは、コンテンツが変更されたときに素早いビルドや更新をするための良いパイプラインが必要なことだね。コンパイルされた静的サイトをS3に部分的にストリーミングする感じ。例えば、何千ものプリレンダリングされた記事を持つ新聞があったとしたら、CMSで著者がコンテンツを編集したときに、単一の記事だけを再コンパイルしたいよね。でも、これにはパイプラインが賢くコンテンツの差分を処理する必要があるってことだね。

このスレッドの盛り上がりには驚きです。ここにいる人たちにとって、これはネットのランダムな人ではなく、ダン・アブロモフだという文脈を知っておくといいかもしれません。彼はReactを構築する上で最も影響力のある人物の一人ですから(確か、創設者の一人だったはず)。

彼は「redux」と「ホットモジュールリロード」で有名になり、その後Metaに雇われてReactの開発に携わり始めました。これはフックの時代の前のことです。

各エントリーがkvペアのリストであるストリームは、同じように機能するでしょうか?その場合、パーサーは受け取ったkvペアを単一のJSONオブジェクトに適用することが期待されます。キーはツリー内のJSONパスを説明するもので、例えば「a.b[3].c」のようになります。

Aftertextを思い出しました。これは、データの以前の部分にマークアップを適用するために逆参照を使用します。これが再帰的にどのように行われるか、またスコープがスパゲッティマークアップを避けるためにどのように機能するかを考えてみてください。Aftertext: https://breckyunits.com/aftertext.html

これをやるためのライブラリとかnpmってあるの? 例え良くないケースでも、部分的なJSONを常にパースするだけでいいんだ。トップだけでも、欠けてる部分があっても、合法なJSONとして常にパースできれば気にしないよ。