コンテンツにスキップ

インタラクティブデモ

ダイヤモンド依存関係のグリッチを実際に見る

Section titled “ダイヤモンド依存関係のグリッチを実際に見る”

2つの派生値が同じソースに依存している場合(ダイヤモンド依存関係)、一部のリアクティブライブラリは不整合な中間状態(グリッチ)を発行します。このデモでは、その違いを自分の目で確認できます。

各キャンバスをクリックしてドラッグし、軌跡を描いてください。各ドットは、同じダイヤモンド依存関係グラフを使用するライブラリのリアクティブパイプラインによって配置されます:

mousePos (source: {x, y})
├── derivedX = map(pos => pos.x)
├── derivedY = map(pos => pos.y)
└── combine([derivedX, derivedY])
└── draw dot at {x, y} + check consistency

発行された各位置は最新のマウス位置と比較されます:

  • 青いドット — 発行された (x, y) が実際のマウス位置と一致(整合)。
  • 赤いドット — 一方の軸は更新されたがもう一方はまだ(グリッチ — ドットがドラッグパスから外れて表示される)。
ライブラリグリッチ理由
SynState0derivedX と derivedY がどちらも深さ順に再計算されてから、combine と subscriber が実行される
RxJS多数combineLatest は derivedX が変わった時点で、derivedY が前の位置のまま発行し、その後もう一度発行する
Jotai0購読している atom は読まれたときに、両方の入力の後で再計算される
MobX0reaction はマウス位置を書き換えたアクションの後、すべての入力が最新になってから computed 値を読む
Preact Signals0effect が computed 値を読み、各 computed はソースのどれかが新しいバージョンなら読まれた時点で再計算される
Solid0書き込みの下流のメモをすべて印付けし、メモを再実行してからエフェクトが実行されるので、両方の新しい値が見える

このダイヤモンドでは 6 つのうち 5 つがグリッチフリーです。5 つとも、派生値については保証があるからです。違いが出るのは、リクエストを開始するステップのように、値を書き込むエフェクトや reaction として書かれたステップです。その場合は Signals の上の operatorで扱っています。また RxJS の余分な発行はグラフが大きくなるほど増えます。次のデモでその回数を数えます。

実際のアプリケーションでは、ダイヤモンド依存関係は、共有状態から算出されるスタイル、派生 UI 値、フォームバリデーションなど、あらゆる場面で現れます。そしてグリッチは以下を引き起こします:

  • 視覚的なちらつき — UI がありえない中間状態をレンダリングする。
  • 無駄な作業 — 余分な発行のたびに不要な副作用が実行される。combineLatest に NN 個の入力がある場合、ソース1回の更新で N+1N+1 回発行される — グラフが大きくなると O(N2)O(N^2) の急激な性能劣化を引き起こす(以下のデモで実演)。
  • ロジックのバグ — 不整合なデータで副作用が実行される。

SynState を含むグリッチフリーのライブラリは、これらの問題をライブラリレベルで排除しているため、手動の回避策は不要です。


ディープ依存関係チェーン: 負荷時の伝搬

Section titled “ディープ依存関係チェーン: 負荷時の伝搬”

このデモは、6 つのライブラリそれぞれで同じリアクティブグラフを構築します: 各ノードが前のノードに依存する深さ NN のチェーンで、最終的な combine/derive がすべての N+1N+1 個の出力を読み取ります。

ヘビの尻尾がマウスを追従します — 各セグメントはチェーン内のステージで、線形補間(lerp)を使用して前のステージにスムーズに追従します。ヘビ全体を描画するために、すべての N+1N+1 個の出力(head + NN ステージ)が1つの combine に集約されます:

上図は N=3N = 3 の場合です。デモではスライダーで NN を調整できます。チェーンの深さを増やして、Total updates カウンターを比べてください。

  • Total updates は各ライブラリの subscriber が呼ばれた回数です。6 つのうち 5 つは、NN にかかわらずマウス移動 1 回につき 1 回です。
  • RxJS はマウス移動 1 回につき約 N+1N+1 回で、NN が大きくなるほどカウンターが引き離され、高い NN ではキャンバスがカクつきます。
  • Avg μs/update は、subscriber の各呼び出しが蛇を描くのにかかる時間です。伝搬は含まないので、どのライブラリでもほぼ同じで、違いは呼び出しの回数に出ます。この形のチェーンを伝わる伝搬はベンチマークのディープチェーンシナリオで計測しています。

高い N で RxJS が急激に劣化する理由

Section titled “高い N で RxJS が急激に劣化する理由”

Total updates カウンターを確認してください: RxJS は同じマウス移動に対して SynState の約 N+1N+1 倍多くの更新を示します。combineLatest は N+1N+1 個の入力のいずれかが値を発行するたびに実行されます。1回のマウス移動で、scan チェーンは順次伝搬します:

  1. source が発行 → combineLatest が実行(source のみ更新、scan はまだ古い値)
  2. scan₁ が発行 → combineLatest が再び実行
  3. scan₂ が発行 → また実行
  4. … すべての NN ステージについて

これらの N+1N+1 回の実行それぞれが N+1N+1 点のフルキャンバス再描画をトリガー → O(N2)O(N^2)。これは最初のデモで示したグリッチ問題が性能面で顕在化したものです。

状態を持つ段の書き方はライブラリごとに異なり、2列目がこのデモでの書き方です。各ライブラリの一般的な伝搬の仕組みは評価モデルにあります。

ライブラリこのデモでの段マウスイベントごとの subscriber 呼び出しイベントごとの総作業量
SynStatescan operator、ルートから深さ順にプッシュ1(combine)O(N)O(N)
RxJSscan operator、購読ごとに深さ優先でプッシュN+1N+1(combineLatest)O(N2)O(N^2)
Jotai1つ前から導出する selectAtom の atom1(store.sub)O(N)O(N)
MobX1つの reaction が1つの action で書き込む box1(autorun)O(N)O(N)
Preact Signals1つの effect が1つの batch で書き込む signal1(effect)O(N)O(N)
Solid前の値を受け取る memo、印付けの後に再実行1(createEffect)O(N)O(N)

RxJS 以外のライブラリはすべてマウスイベントごとに subscriber を1回だけ呼び出すので、作業量はどれも NN に比例します。その中で 1 回の更新にかかるコストは、ライブラリとグラフの形によって決まる定数です。次のデモでそれが見え、ベンチマークで計測しています。


スループット: 負荷時の更新ごとのオーバーヘッド

Section titled “スループット: 負荷時の更新ごとのオーバーヘッド”

このデモでは、O(N2)O(N^2) の更新回数が支配的になる RxJS を除き、更新ごとに subscriber を 1 回だけ呼ぶ 5 つのライブラリを実行します。

ボールが自動的に円軌道を周回します。各キャンバスは上記のスプリングデモと同じ深さ MM のリアクティブチェーンを実行します。違いは、マウスイベントごとに1回のソース更新ではなく、各アニメーションフレームで KK 回のソース更新が同期ループでチェーンを通じてプッシュされることです。

each animation frame:
for k in 0..K-1:
update source position (micro-step along orbit)
→ propagate through depth-M scan chain
→ subscriber records latest points
draw snake once

これにより更新ごとのオーバーヘッドが KK 倍に増幅されます。更新毎の処理時間が 16ms を超えると、アニメーションはフレーム落ちしてカクつきます。

  • ms/frame は 1 フレームの KK 回の更新にかかった時間で、このブラウザとこの端末で計測した値です。60fps の 1 フレームの予算である 16ms を超えると赤に変わります。
  • KK と MM をキャンバスがカクつくまで上げてみてください。どれが先に、どの KK でカクつくかはブラウザ、端末、ページの他の処理で変わるので、ここの数値はベンチマークではありません。同じグラフを条件をそろえて実行したものはベンチマークのディープチェーンシナリオにあります。
  • 各ライブラリが更新ごとに何を、いつ行うかは評価モデルにあります。