性能
このアーキテクチャは具体的な主張をしています。「コントロールを DOM に置いても規模の面で代償はない」というものです。このページはその根拠と、自分のアプリケーションで確かめる方法です。
目標値
| 指標 | 目標 |
|---|---|
| 5,000 図形でのパン・ズーム | 60fps |
| ヒットテスト | 1ms 未満 |
| オーバーレイの DOM ノード数 | 一定。表示中のコントロールの関数であり、文書の関数ではない |
最初の2つはハードウェア依存でブラウザ上での実測です(上のデモが両方を表示します)。3つ目は構造上の性質で、構成そのものによって成立します。
DOM オーバーレイがボトルネックにならない理由
コントロール DOM は選択中の対象にしか存在しません。 図形が1万個あって1個選択されていれば、枠1個とハンドル9個です。複数選択でも枠は1個。ノード数は「存在するもの」ではなく「操作しているもの」に比例します。
ビューポート変換は1要素に1回だけ書き込みます。 オーバーレイの子はワールド座標に置かれ、カメラが動いても触られません。パンのコストはコントロールが3個でも30個でも同じです。
ハンドルの大きさは CSS で --hc-zoom による除算で補正します。 代替案 — 毎フレーム各要素に補正済みのピクセル値を書き込む — は、直前の項目で取り除いた「要素ごとの処理」をそのまま戻すことになります。逆補正が推奨ではなく規則である理由がこれです。
カリング
表示範囲外の図形は描画されません。ヒットテストに答えるのと同じ R-Tree が可視性の問い合わせにも答えるため、既存のコストにほとんど何も上乗せしません。
editor.getRenderStats() // { drawn: 214, culled: 4786, indexed: 5000 }
editor.getVisibleShapeIds()一時状態にある図形は決してカリングされません。そうでないと、画面外からドラッグしてきた対象が、まさに必要な瞬間に消えてしまいます。
ヒットテスト
2段階です。R-Tree が外接矩形で候補を絞り、ShapeUtil.hitTest が厳密に判定します。
どちらも必要です。回転した図形の外接矩形は図形本体よりずっと大きいため、矩形判定だけでは何もない場所で選択が成立します。一方、全図形を厳密判定すれば文書サイズに比例します。候補は描画順の逆で走査するので、最前面の一致が勝ち、そこで探索が止まります。
許容誤差はスクリーンピクセルで受け取り、問い合わせごとに変換されます。1px の線がどのズームでもクリックできるのはそのためです。
描画
描画は requestAnimationFrame に合体されます。1ティック内でいくら状態を変えても1フレームです。同期的に描画されることはないため、updateShape のループが N 回描画することはありません(履歴のためにトランザクションには入れるべきですが)。
ダーティ矩形による描画は v1.0 にありません。毎フレーム可視集合を描き直すため、カリングが要となる最適化になっています。
速いツールの書き方
もっとも重要な規則は1つです。操作中は一時状態に書き、最後に1回コミットする。
// ドラッグ中、毎フレーム1回
editor.setEphemeral(new Map([[id, { x, y }]]))
// ポインタアップ時
editor.commitEphemeral()確定状態を毎フレーム書くと、イミュータブル木を毎秒60回作り直し、かつ Undo スタックを埋め尽くします。この2つは同時に解消します。一時状態の層が後付けの最適化ではなく最初から存在する理由がそれです。
一時状態の図形は空間インデックスから除外され線形に扱われます。同時に動くのは高々100個程度という前提です。数千個を同時にドラッグするアプリケーションなら、依拠する前に実測してください。
トランザクション
editor.transact(() => {
for (const row of tenThousandRows) editor.createShape(toShape(row))
})通知1回、履歴1件、インデックス再構築1回。囲まなければ、それぞれ1万回です。
バンドルサイズ
core と ui は実行時依存ゼロです。R-Tree、ID 生成、fractional index はいずれも npm から取らずリポジトリ内で実装しています。小さく安定した3つのアルゴリズムか、3つのサプライチェーン面と3組の推移的依存かの選択です。
core の公開目標は gzip 30KB ですが、これは厳密な上限ではなく目標として扱われます。CI は超過を警告し、はるかに大きく超えた場合にのみ失敗します。実際に気にすべき数値は、誰も丸ごと import しない barrel のサイズではなく、tree-shaking 後のあなたのバンドルです。sideEffects を宣言しているので、使っていないシェイプ・ツール・ユーティリティは落ちます。
自分のアプリケーションで測る
editor.getRenderStats() // 描画 / カリング / インデックス
editor.overlayElement.querySelectorAll('*').length // 図形数に比例してはならない
editor.getSnapshot().shapes.sizeオーバーレイのノード数が文書とともに増えているなら、図形ごとにコントロール DOM を作っている何かがあります。それは不変条件2が破れている状態であり、DOM オーバーレイが無料でなくなる地点です。