Skip to content

仮想スクロール

なぜエンジン側で行うのか

DOM ベースの仮想リストのテールレイテンシは、スクロールイベントがメインスレッドに戻り、 setState を起こし、差分を取り、再レイアウトするところから来ます。メインスレッドが忙しければ フレームは落ちます。

doper はウィンドウ計算を Core に置きます。スクロール中はまったく Shell を呼びません。 Shell は Core が計画したプリフェッチウィンドウに従って可視区間を実体化するだけで、データが まだ無ければプレースホルダーを描き、後続フレームで補完します。

使い方

ts
createElement("virtualList", {
  width: 480,
  height: 640,
  itemCount: 1_000_000,
  estimatedItemHeight: 32,
  renderItem: (index: number) =>
    createElement("container", {
      width: 480,
      height: 32,
      children: createElement("text", { value: `${index} 行目` }),
    }),
});

estimatedItemHeight はあくまで初期見積もりです。実際の高さを計測すると、Core が累積和ツリー (Fenwick)でアンカー位置を補正するため、スクロール位置が飛ぶことはありません。

調整できる項目

prop役割
baseOverscanViewports対称なプリフェッチ範囲(ビューポート倍数)
velocityHorizonSeconds方向予測に使う速度の投影時間
maximumAheadViewports片方向のプリフェッチ上限
scrollX / scrollYプログラムからのスクロール位置(変化時のみ ScrollTo)

方向予測は速いフリック時に進行方向を優先してプリフェッチし、両側に予算を均等に浪費しません。

プログラムからのスクロール

ts
// prop の変化で ScrollTo mutation を 1 回発行します
root.render(createElement("virtualList", { scrollY: 500_000 * 32 /* ... */ }));

カスタムジェスチャ向けには root の直接操作 API を使います。

ts
root.beginScroll(handle);
root.scrollBy(handle, 0, deltaY, elapsedMs);
root.endScroll(handle); // フリング速度の推定は Core に任せます

handle は要素の ref コールバック(NodeHandle)から得られます。

ホイールとトラックパッド

ホイールの移動量はブラウザネイティブと一致しますが、伝達カーブは入力ソースで分岐します。 高精度デルタ(トラックパッド)はそのまま 1:1 で即座に適用し、慣性は OS のイベント列が供給します。 離散的なホイールノッチはアニメーションの目標値に積み上げ、指数的なイーズアウトで近づけます。 ブラウザと同じくコンテンツ境界にハードクランプし、オーバースクロールは起こしません。

入れ子と編集

ポインタのドラッグが編集可能テキストの上で始まった場合はテキスト選択がスクロールドラッグより 優先されます。ホイールは引き続き最も近いスクロール祖先をスクロールします。この優先順位は ヒットパスの深さで決まり、アプリケーション側の介入は不要です。

性能の基準

固定フィクスチャ(100 万行、20,000 フレーム)の自動ベンチマークはマージゲートの一部です。 現在の P95/P99 はサブマイクロ秒の再生で、30 分の連続スクロールでも制御不能なメモリ増加はありません。

実機の P95/P99 と入力遅延はプラットフォーム認定として収集し、エンジニアリングの完了条件には しません。この線引きは意図的で、再現できない実機データで開発を止めないため、そして エンジニアリングの数値を実機の約束として偽らないためです。

Playground のスクロールデモでリアルタイムのフレーム指標を見られます。

MIT ライセンスで公開