高速データグリッドのエンジニアリング:1M+データレコードのIgnite UI最適化からの教訓
グリッド性能は単なる速度だけではありません。大切なのは、重いデータ負荷下での一貫性の問題です。データ処理中にグリッドがフリーズすると、動作が遅く信頼性に欠けると感じます。リアルタイムの意思決定ワークフローにおいて、その信頼性の欠如はリスクとなります。
金融、銀行、ERP、その他のデータ負荷の高いシステムを構築する開発者にとって、データグリッドはしばしば主要なパフォーマンス境界線、つまり大規模なデータセット間のソートやフィルタリングがメインスレッドの時間を奪い合う「ホットループ」となります。こうした場合、小さな非効率がすぐにユーザーに見えてしまい、インタラクションを断ち切ってしまいます。
でも解決策を見つけました。本記事では、フレームワーク(Angular、React、Blazor、Web Components)で1M+行を高速Ignite UIに保つために、ソートとフィルタリングをどのように最適化したかを説明します。うまくいった変更点とそうでなかった変更点に焦点を当てます。
さて、私たちが何をしたか見てみましょう。
最適化前の現実:どこから問題が始まったのか
すべてのパフォーマンス問題は同じように始まります。あるスケールで妥当だったアーキテクチャが、別のスケールでボトルネックになるのです。Ignite UIのソート、グループ化、フィルタリングなどの機能も例外ではありませんでした。
ソーティング:価値の隠れたコスト解決
コアソートパイプラインは再帰的に動作し、各ソート式を順番に処理しました。多列ソートの場合、主式でソートした後、等価レコードをグループ化し、次の式で再帰的にソートしました。クリーンで正確、そして小さなデータセットに対しては完全に妥当です。
問題は価値解決装置でした。
グリッドは複数の列データ型(Dateオブジェクトの日付部分、Dateオブジェクトの時間部分、文字列、数値、階層キー-valueオブジェクト)をサポートしているため、すべての値比較では実行時にフィールド値を解決する必要がありました。値リゾルバはパストラバーサル、日付解析、時間正規化、数値解析など、すべての比較で処理しました。比較演算ごとに2回、各側に1回ずつ呼ばれました:
compare(recordA, recordB):
valA = resolveValue(recordA, field) // path traversal + date parsing + type coercion
valB = resolveValue(recordB, field) // same cost, every single comparison
return compareValues(valA, valB)
標準的な比較ソートでは、比較を行い、リゾルバは1回の比較で2回呼び出されます。10万行の場合、ソート済み列あたり340万回のリゾルバ呼び出し。100万行で4,000万件のリゾルバー通話。それぞれが実行時パス解析と潜在的な日付解析を行い、呼び出し間のキャッシュはありません。
しかし、ソート比較器だけが値リゾルバが呼び出されたわけではありません。多列ソートの場合、式iでソートした後、アルゴリズムは等しい値のグループを見つけてから式i+1でソートする必要がありました。このグループ検出はすべてのレコードを反復し、レコードごとにリゾルバを1回呼び出し、つまり追加の処理を行いますその上にパスを送りましょう。
したがって、1M行の2列ソートの場合、値リゾルバは次の順序で呼び出されました。 + 最初の表情だけの時間――二つ目の表情に触れる前から。
- 1万行ではほとんど気づかれません。
- 10万行で明らかな遅延がありますが、我慢できる範囲です。
- 1M行でメインスレッドが数秒間フリーズしました。まれに、ディープ再帰呼び出しスタックがスタックオーバーフローを引き起こしました。
グループ化:同じ根、複利費用
グループ化は同じ再帰パターンを拡張し、まずデータをソートする必要があります。この方法により、リゾルバのコストはソート時に一度支払い、グループ境界検出時にもう一度支払われました。
groupDataRecursive(data, state, level):
while i < data.length:
group = groupByExpression(data, i, expressions[level])
// resolver called once for group anchor value
// resolver called again for every subsequent record in the group
if level < expressions.length - 1:
groupDataRecursive(group, state, level + 1) // recurse into subgroups
else:
result = result.concat(...) // array allocation per group boundary
ここには複利的なコストが2つあります:
- 値リゾルバはソート時に既に解決済みの値に対して繰り返し呼び出され、2つのフェーズ間で共有キャッシュはありませんでした。
- 各グループ境界は、コンカト(concat)やスライス(スライス)による新しい配列を生み出し、数千のグループにまたがるスケールで測定可能なGC圧力を加える 割り当てを行った
Excelスタイルのフィルタリング:全額を二度支払うこと
クイックフィルタリングも高度なフィルタリングも速かったです。しかしExcelスタイルのフィルタリング(ESF)はそうではなく、その理由はアーキテクチャ上のものでした。
ESFダイアログが開くと、メインスレッドで同期的に完全な初期化パイプラインが起動しました:

ダイアログの冒頭アニメーションは、4つの操作すべてが完了するまで事実上一時停止されていました。大規模なデータセットでは、ユーザーが見たフリーズが起こり、ダイアログがぎこちなく見えませんでした。パイプラインが完成するまで、まったく現れなかったのです。
より重大な問題は、ユーザーが「Apply」をクリックすると、基礎データが「開いた」と「適用」の間で変わっていないにもかかわらず、このパイプライン全体が再び実行されたことです:
onApplyClick():
filter data
re-run full ESF initialization // same 4 steps, same cost, same blocking
close dialog
これがESFが実際には高度なフィルタリングよりもかなり遅かった理由です。ESFも同じことをしていたのです1回の操作で2回作業し、両方ともメインスレッドをブロックします。
なぜ「もっと仮想化する」というのが答えではなかったのか
仮想化はデータセットのサイズに関わらず、DOMノードとしてレンダリングされる可視行の数のみを保証します。だからこそ、100万行をスクロールして移動することが可能になるのです。しかし、その行に含まれるものを決定するデータ操作(ソート、フィルタリング、グループ化)は、毎回全体のデータセットに対して実行されます。仮想化はその点では役に立ちません。上記のすべてのボトルネックは、1行がレンダリングされる前にデータパイプライン内に存在していました:
- リゾルバは + ソート式ごとに、何行が表示されていても、1回のソート表現に時間がかかります。
- グループ化はソートに加えて解決コストを再び支払い、グループ境界をまたぐコンキャット/スライスの割り当て圧力も負担しました。
- ESFの初期化パイプライン全体は、オープン時と適用時の同期的にデータセットを反復処理しました。
仮想化は、大きなグリッドをスクロール可能にする適切なツールです。ソートやフィルタリング、グループ化を素早くする効果はありません。それらは別の種類の修理が必要でした。
問題の測定:グリッド性能のベンチマーク方法
「遅いと感じる」「速く感じる」といった逸話は診断ではなく出発点です。自信を持って最適化するためには、インプレッション数の代わりに再現可能な数値が必要でした。
グリッド性能の診断にDevToolsのフレームグラフやFPSカウンターに頼りたくなる誘惑があります。しかし、これらはレンダリングパイプライン全体を測定します。変更検出、DOMの更新、レイアウトなどで、実際にデータパイプラインに費やされている時間が見えにくくなることがあります。
アルゴリズムコストを特定するために、ソート、グループ化、フィルタリングロジックをネイティブPerformance APIの軽量ラッパーで直接インスルメント化しました:
startMeasure(‘sorting’)
-> run sorting algorithm
getMeasures(‘sorting’) // returns the duration
これにより、ノイズや変化検出のオーバーヘッドなしに、アルゴリズムのサブミリ秒単位のタイミングが分離されました。純データパイプラインのコストだけです。注目すべきは、以下の数字はすべてAngular開発者モードで記録されたことです。本番環境のビルドは速いですが、開発者モードのオーバーヘッドはランごとに一貫しているため、相対的な差は成立します。
データセット
Rows:
10K / 100K / 1,000,000
Columns:
string - names, categories (with duplicates)
number - IDs, prices, quantities (with duplicates)
date - formatted date strings (require parsing)
time - HH:mm:ss formatted strings (require parsing)
ソートおよびグループ列に重複値が存在するのは意図的であり、現実的なデータ分布を反映し、重複する値が多いほどグループ境界検出やより深い再帰呼び出しが増えるため、グループ化コストに直接影響します。日付と時刻の列は書式化された文字列表現を用いていました。これは結果の解釈に重要です。これらの列を比較する際は、実行時に文字列を比較可能な値に解析する必要があります。
Scenarios and Results
10Kおよび100K行では、ほとんどの操作が許容範囲でした。100万行に達した時点で状況は劇的に変わりました。
| Scenario | 時間(1M列) |
| Single column sort – string | 3.38s |
| Single column sort – number | 1.50s |
| Multi-column sort – string → number | 3.88s |
| グループ化 – 単一文字列の列(ソート+グループ) | 3.31s |
| グループ化アルゴリズムのみ(ソート後) | 0.50s |
| グルーピング – グリッド負荷に2列 | 3.86s |
| グループ化 – 2列(ソート後に) | 1.01s |
| ESF open – number column (15K unique values) | 1.60s |
| ESF open – date column (274 unique values) | 5.20s |
| ESF open – time column (86K unique values) | 6.60s |
| ESF apply – number column | 1.37s |
数字を読む
いくつかのパターンがすぐに現れ、それぞれが特定のアーキテクチャ上の問題を直接指し示しています。
ソートがグループ化コストを支配します。グルーピングアルゴリズムだけでも0.50秒かかりました。フルソート+グループは3.31秒で、6.6倍の差でした。グループ化の論理自体がボトルネックではなかった。ソートが行われ、特に値リゾルバが呼ばれていましたソート比較器内の時間。
文字列ソートは数字ソートの2倍以上遅く(3.38秒対1.50秒)、数字は単純な引き算と比べられます。文字列は値リゾルバ、大文字に気分ないソートの正規化、文字列比較を経ます。その差は、100万行あたり約2,000万回の比較にまたがって累積します。
ESFの日付異常が最も示唆に富むデータポイントです。日付列には274のユニークな値しかなく、数字列の15Kに比べて非常に小さなリストです。しかし、ESFのダイアログを開くと数字欄は5.20秒かかり、数字欄は1.60秒でした。原因は反復回数ではありませんでした。それはアイテムごとの日付解析コストでした。ESF初期化時に全データセットを反復処理し、すべての値は現在までの文字列解析を経ました。ユニークな値が少なかったのは、解析がユニークレコードだけでなくすべてのレコードで行われていたからです。時間列(6.60秒、86Kのユニーク値+時間文字列解析)も同じパターンを示しています。つまり、フォーマットされた文字列列は濃度に関係なくコストがかかります。
ESFオープン+ESF適用=全額支払いを2回します。数値列の場合、最も安価なケースは1.60秒 + 1.37秒 = ~3秒のブロッキングです。日付や時間の欄では、合計コストはかなり悪くなります。
数値はアーキテクチャレビューが示唆していたことを裏付けています。すなわち、値リゾルバ、再帰的グルーピングパス、ESFのダブル初期化がボトルネックでした。今やそれを証明するデータが手に入った。
最適化 #1:ソーティングパイプラインの再考
明確な基準が確立されると、焦点はデータパイプライン自体に移った。改善の大部分を推進したのは3つの変更点です。シュワルツ変換をソートに適用すること、再帰的から反復方式への多列ソートのリファクタリング、そして再帰と冗長な配列割り当てを排除するためのグループ化アルゴリズムの再構築です。
Fix #1: The Schwartzian Transform
元のソート比較器は比較関数内でフィールド値を解決していました。つまり、比較されたレコードのペアごとに値リゾルバは2回実行されました。
シュワルツ変換は高価なソートキーの古典的な最適化方法で、各値を一度最初に解決し、キャッシュされた値をソートし、元のレコードに戻す方法です。これにより、宛先:
// Before: resolve inside comparator - O(n log n) resolver calls
sort(data, field):
data.sort((a, b) => compare(resolveValue(a), resolveValue(b)))
// After: Schwartzian transform - O(n) resolver calls
sort(data, field):
prepared = data.map(record => [record, resolveValue(record, field)]) // O(n) - resolve once
prepared.sort(([, valA], [, valB]) => compareValues(valA, valB)) // O(n log n) — compare only
return prepared.map(([record]) => record) // O(n) - unwrap
比較器はフィールド分解なし、経路トラバーサルなし、日付解析なしの純粋な値比較となります。ignoreCaseの場合、文字列正規化コールはマップフェーズに移行し、比較側ごとに1回ではなくレコードごとに1回解決されます。
日付と時間の列では、特に影響が大きいです。ストリング・トゥ・デート解析はホットコンパラーループ内から単一の事前パスへと移行します。100万行の場合、約4,000万回の解析呼び出しと正確に100万回の差、つまり列の種類に関係なく、乗数は一定1です。
Fix #2: Iterative Multi-Column Sorting
元の多列ソートは再帰的で、式0でソートし、同じ値のグループを見つけ、各グループを式1で再帰的にソートするという繰り返しです。その通りですが、2つの問題があります。再帰的な呼び出しスタックの深さと、すべてのレコードのグループ検出内で値リゾルバが再度呼び出されることです。
新しいアプローチは式を逆算して繰り返しますが、これは元の再帰実装の挙動に合わせたソートの安定性を維持するための意図的な選択です。
// Before: recursive
sortDataRecursive(data, expressions, index):
sort by expressions[index]
for each equal-value group:
sortDataRecursive(group, expressions, index + 1) // recursive
// After: iterative - reverse pass maintains stability
sortData(data, expressions):
for i = expressions.length - 1 down to 0:
data = expressions[i].strategy.sort(data) // iterative, no recursion
逆に反復する場合は、最重要ソートキーが最後に適用されます。これが最終タイブレーカーとなり、全体の順位は安定しています。再帰的な呼び出しスタックはなく、式間の中間グループ検出パスもなし、追加のリゾルバ呼び出しもありません。シュワルツ変換は各式パスに独立して適用されます。
修正 #3:スタックによる反復的グループ化
グルーピングアルゴリズムには、再帰的呼び出し構造と各グループ境界でのコンカット/スライス配列割り当てという2つの独立したコスト源がありました。両者は同時に呼びかけられました。
// Before: recursive with concat/slice
groupDataRecursive(data, state, level):
group = data.slice(start, end) // allocation per group
result = result.concat(groupRow, group) // allocation per group
groupDataRecursive(group, state, level + 1) // recursive
// After: iterative with explicit stack + direct push
groupData(data, state):
stack = [{ data, level: 0 }]
while stack.length > 0:
{ data, level } = stack.pop()
for each group boundary in data:
result.push(groupRow) // no intermediate allocation
result.push(...groupRecords) // no intermediate allocation
if level < expressions.length - 1:
stack.push({ data: groupRecords, level: level + 1 })
ここではアレイの事前割り当ては、グループ数が事前に分からないため実現不可能でした。しかし、コンキャット/スライスからダイレクトプッシュに切り替えると、すべてのグループ境界で中間配列の割り当てがなくなります。大規模に、数千のグループ境界をまたぐ場合、実行時間とGC圧力の両方に測定可能な違いをもたらしました。
結果

生のミリ秒単位が物語の一部を語っています。より重要な指標は、反応性の知覚です:
- 1M行の単一列文字列ソートは、3.38秒(目に見える不快なフリーズ)から0.42秒に上がり、ほとんどのユーザーに気づかれないほどです
- 多列ソートは3.88秒から0.57秒に減少し、連続ソートを適用したユーザーは複利遅延を経験しなくなりました
- グリッド負荷の2列グループ化は3.86秒から0.88秒に上がり、グリッドはほぼ即座に準備が整ったように感じられます
実際の使用ではその利点が増大します。ソートしてからグループ化し、再ソートするユーザーは、もはや各操作に数秒待つ必要がなくなりました。パイプラインは十分に速く進むため、インタラクションがフリーズで途切れることはなく、連続的に感じられます。
最適化 #2:大規模にエクセルスタイルのフィルタリング
ソートやグループ化が最も目立つボトルネックでしたが、Excelスタイルのフィルタリングには独自の問題がありました。クイックフィルタリングと高度なフィルタリングはデータを直接処理します。述語は各レコードに対して実行し、一致を返します。シンプルで、直線的で、予測可能。
Excelスタイルのフィルタリングは異なります。ダイアログが表示される前に、列内のすべての一意値を含み、表示用にフォーマットされ、ソートされ、現在のフィルター状態と照合されたデータの完全な全体像を構築する必要があります。それは単なるフィルタリング作業ではありません。これは完全なデータパイプラインで、ダイアログが開くたびにメインスレッド上で同期的に動作していました。
前述の通り、元のExcelスタイルのフィルタリング初期化では、データを4回連続的にパスしました。
- 事前にフィルターが適用されている場合は、データセットをフィルタリングしてください。 pass
- フィルターされた値を並べ替えてください –
- ラベル+フォーマット値の抽出 – pass
- Deduplicate -> build unique items list – pass
Applyの再初期化が最も無駄な部分でした。基礎データはオープンと適用の間で変わっていなかったのに、パイプライン全体が一からやり直されていました。
二重コストに加え、パイプライン自体も非効率性がありました。ステップ2、3、4はすべてフルフィルタリングされたデータセット上で動作していました。ソートは重複除去以前に行われており、グリッドは一意値だけをソートすればよいのに、何百万ものレコードをソートしていた可能性があります。ラベル抽出と重複除去も同じデータに対して別々のパスであり、すべての値を不必要に2回訪問しました。
日付と時間の異常
非効率性は、日付と時間の列で最も顕著に表れていました。『Measuring the Problem』のベンチマークから:
| 縦棒 | Unique values | ESFの開放時間 |
| 番号 | 15k | 1.60s |
| 日付 | 274 | 5.20s |
| 時間 | 86k | 6.60s |
日付列には274のユニークな価値があり、数字列の15,000件よりはるかに少なかったのに、開封までに3×時間がかかりました。理由は、ラベル抽出や値のフォーマットがデータセット全体の日付解析を含み、一意な値を解析するだけでなく、すべての記録が訪問され、訪問ごとにストライク・トゥ・デ・コンバージョンが引き起こされました。一意の数値が少なかったのは、解析が重複除去後ではなくフルデータパス時に行われたため、助けにはなりませんでした。
修正 #1:ダブル初期化を排除する
最も大きな変化は構造的なものでした。ESFはApplyで再初期化されなくなりました。Open上で作成されたユニークな値リストは、ユーザーが「適用」をクリックした際に直接再利用されます。2回目のパイプラインの完全な稼働は完全に消えました。
// Before
onApplyClick():
re-run full ESF initialization // O(n) - redundant
close dialog
// After
onApplyClick():
apply filter using existing list // O(1) - list already built
close dialog
修正 #2:遅延ソーティングによる単一パス重複除去
2つ目の変更ではパイプラインが完全に再構築され、ラベル抽出と重複除去を一つのパスにまとめ、重複除去された結果のみをソートしました:
// Before: separate passes
filteredData → sort → extract labels (pass 1) → deduplicate (pass 2)
// After: deduplicate in single pass → sort unique list only
filteredData (n records)
→ single pass:
resolve + normalize + deduplicate inline // O(n), parse only for new unique values
→ unique list (m items)
→ sort unique list // O(m log m) where m <= n
ここには二つの複合的な改善があります:
- ラベルのフォーマットや日付解析は、データセット内のすべてのレコードに対してではなく、ユニークな値に対してのみ実行されます。1M行データセットで274個のユニークな日付列の場合、1Mの解析呼び出しと274の差です。
- ソートは重複除去されたリスト上で行われ、フルフィルタされたデータセット全体ではなくなりました。274のユニークな数値があるため、ソートは事実上瞬時に行われます。86Kの一意値を持つ時間列でも、86Kのアイテムをソートするのは1Mのソートよりも桁違いに安価です。また、各ソートの比較にはタイム文字列解析が必要なため、ソート入力を縮小することでさらに節約効果が増します。
Fix #3: Non-Blocking Dialog Open
3つ目の変更は、パフォーマンスの認識に直接対応したもので、データパイプラインが実行される前にダイアログが即座に開かれるようになった。初期化が完了するとロードインジケーターが表示されます。つまり、UIがまだ表示されていないダイアログを待つためにフリーズすることは決してありません。初期化に時間がかかっても、ユーザーは即座にフィードバックを得られます。ダイアログが開かれ、何かが起きているのです。
Fix #4: Debounced Quick Filtering
素早いフィルタリング面での小規模ながら意味のある改善点として、以前はフィルタリングパイプがキーストロークごとにトリガーされ、「Finance」と入力すると7回連続でフィルター操作が起こり、それぞれが全データセットを繰り返し処理していました。
// Before: filter on every keystroke input: "F" → filter // O(n) input: "Fi" → filter // O(n) input: "Fin" → filter // O(n) ... // After: debounced input: "F", "Fi", "Fin", "Fina", "Finan", "Financ", "Finance" → pause detected → filter once // O(n) - only when user stops typing
大規模なデータセットの場合、これだけで典型的な検索におけるメインスレッドフィルター操作数を5〜10回から1〜2回に減らします。
結果

ESFの適用数は特に重要で、90msでクイックフィルタリングやアドバンスドフィルタリングと同じ性能範囲に入っています。3つのフィルタリングモードは初めてコストが同等になりました。
これが実際に意味すること
- ESFのダイアログはクリックするとすぐに表示されます。表示されない会話を待つ必要はありません。
- ESFダイアログ内のデータの読み込み時間は、すべてのカラムタイプでより速く感じられます。データセットが大きくても、ユーザーは読み込みインジケーターをじっと見る時間が減ります。
- フィルターを適用しても初期化コストは完全に繰り返されません。以前と比べて実質的に無料です。
- クイックフィルタリングはもはや高速タイピングのメインスレッドを押し付けるものではありません。デバウンスは、ユーザーが終了または一時停止した場合にのみパイプラインが動作するようにします。
なぜこれらの変更がフレームワークを超えて機能するのか
上記のパフォーマンス改善は、Angularコードベースで行われました。しかし、彼らはそこに留まらない。
One Core, Multiple Frameworks
Ignite UIのグリッドはAngularに組み込まれており、ネイティブAngularコンポーネントとして直接使用でき、Angularのテンプレート構文、DIシステム、変更検出に完全アクセスできます。また、Angular Elementsを用いたウェブコンポーネントとしてもパッケージ化されており、完全に外部Angular利用可能です。ReactとBlazorは、カスタム要素のAPIをそれぞれReactプロップとBlazorパラメータにブリッジする薄いフレームワーク固有のラッパーを通じてそのWeb コンポーネントを消費します。
データパイプライン—ソート、グループ化、フィルタリング—は完全にAngularベース内に存在します。Angular ElementsはそれをそのままWebコンポーネントにパッケージ化します。React、Blazor決して触れません。Angularコードベースで加えられるすべてのアルゴリズム的改良は、自動的にチェーン全体に伝わります。ここで「ラッパー」が何を意味するのかを正確に説明する価値があります。これは薄い統合層であり、再実装ではありません。
なぜアルゴリズムの改善がフレームワークに依存しないのか
シュワルツ変換、反復グルーピングスタック、そして単一パスESF重複除去は純粋なデータ操作です。彼らはアレイを受け入れ、変換したアレイを戻します。彼らはAngularの変更検出、Reactのリコンシラー、Blazorのレンダリングツリーを全く知らない。だからこそ、これらが4つのプラットフォームすべてできれいに伝播しているのだ。
改善点はJavaScriptエンジンの向上です:
- Fewer resolver calls per sort operation.
- グループ境界あたりの中間配列割り当てが少なくなります。
- Less GC pressure across the full pipeline.
- すべてのデータ操作でのメインスレッドブロッキング時間が短縮されます。
これらはいずれもフレームワークの概念ではありません。より速いソートは、結果がAngular、React、Web Components、またはBlazorでレンダリングされる場合でもパフォーマンスを向上させます。なぜなら、最適化はUIフレームワークがレンダリングする前にデータレイヤーで行われるからです。
どのグリッドを使うかを評価する開発者にとって:エンジンが同じであるため、フレームワーク間でパフォーマンスのストーリーは同じです。この投稿の数字はAngular数字ではありません。それらはデータパイプラインの番号であり、データパイプラインは共有されています。
これがエンタープライズチームにとって意味すること
エンジニアリングのパフォーマンス勝利はミリ秒単位で簡単に測定できます。彼らのビジネスへの影響は定量化が難しいものの、特にエンタープライズ規模でははるかに重要であり、データグリッドは装飾的なUI要素ではなく、アナリスト、トレーダー、オペレーションチームが作業を行う主要なインターフェースとなっています。
データグリッドのパフォーマンス問題は、再現が難しく、診断が難しく、クロージングが難しい、特定のフラストレーションの溜まるサポートチケットのカテゴリーを生み出します。「ソートするとグリッドがフリーズする」はスタックトレースのバグではありません。これは、実際のデータボリュームの下でメインスレッドを数秒間ブロックするパイプラインの症状です。
Ignite UIリモートデータバインディングをサポートし、ソートやフィルタリングはクライアント側ではなくサーバーに委譲できます。クライアント側のパフォーマンスが不十分だったためにリモート運用を採用したチームにとって、これらの最適化は計算方法を変えます。クライアント側のソートは100万行で0.5秒未満で完了します。以前はサーバー側の委任を促していた多くのエンタープライズデータセットでは、クライアント側のパイプラインが今やその決定を再考するのに十分速いのです。
特に金融サービスなどの企業環境では、レスポンシブさの認識がプラットフォームの採用に直接影響します。ソートを3.38秒から0.42秒に変えることは、単に8×の単独での改善ではありません。これは、ワークフローを中断するやり取りと、遅延として認識されないやり取りの違いです。この区別は、エンドユーザーがツールの価値を判断する際に重要です。
教訓:私たちが再び(そして違う方法で)何をするか
この投稿のビフォー・アフターの数字はクリーンです。しかし、それらを生み出したプロセスはそうではありませんでした。そのプロセスの実際の様子は以下の通りです。
最初から保証されたものはありませんでした
この作業に入る時点で、これらの最適化が意味のある結果をもたらすかどうかは確信がありませんでした。シュワルツ変換はよく知られた手法です。しかし、「よく知られている」は「この文脈で必ずに役立つ」という意味ではありません。反復的グルーピングスタックは紙の上では有望に見えましたが、再帰的から反復へのリファクタは、特定のデータ形状下でのみ現れる微妙なエッジケースを導入する歴史があります。
このアプローチは意図的に段階的で、一度に一つの問題に取り組み、測定し、続けるかどうかを決めるというものでした。最初に仕分けのパイプラインが始まりました。数値が戻ってきて、文字列ソートで3.38秒から0.42秒までで、方向性が確認され、グループ化やフィルタリングを続けることが正当化されました。もし最初の最適化でわずかな利益が示されていたら、戦略は変わっていたでしょう。
これは重要な点で、パフォーマンス作業はしばしば結果が事前に知られているかのように計画されているからです。そうではありません。正しい姿勢は仮説、測定、決断、繰り返しです。
メモリのトレードオフ
シュワルツ変換は無料ではありません。[レコード、値]ペアの中間配列を最初に割り当てます。1レコードにつき1エントリずつです。100万行の場合、ソートが始まる前のメモリオーバーヘッドは決して取れません。
これは意図的なトレードオフであり、より高いピークメモリ使用量を受け入れる代わりにリゾルバーが呼びかける。このライブラリが対象となるユースケース、すなわち現代のブラウザで動作するエンタープライズグリッド、対応可能なハードウェアでは、速度向上は大きく、メモリコストも許容範囲内です。
しかし、明確に挙げておく価値があります。もしメモリ制約環境が主要な対象となることがあれば、シュワルツ変換を再検討する必要があります。ここでは速度とメモリが逆方向に引っ張られ、現在の実装では速度が優先されています。
ベンチマークは実際の使用状況を反映しなければなりません
本研究のベンチマークスイートは、100万行の合成データセット(制御された列タイプと値分布を持つレコード生成)を使用していました。それがアルゴリズム性能を分離する適切な出発点ですが、上限があります。
この作業を促した2つの問題は実際の顧客から発生しました。ESFダイアログの開閉時間とESF適用時間が本番環境でのブロッキング問題として報告されました。そのチケットが届いたとき、合成ベンチマークが問題を確認しました。問題はチケットが出る前から存在していました。実際に使われたパターンがそれを明らかにしたのです。
教訓はシンプルです。合成ベンチマークは、すでにテストすべきシナリオを測定するのに優れています。顧客データが、含めなかったものを見つけ出します。どちらも必要であり、ベンチマークスイートは単なる合成的な最悪ケースだけでなく、現れた実際の利用パターンを反映する形で進化していくべきです。
パフォーマンスの仕事は決して終わらない
この投稿の改善は現実的で重要なものです。また、それらは一瞬のスナップショットでもあります。データパイプラインは6か月前よりも速くなっています。6か月後には、日付解析や仮想化など、この作業以前のソートパイプラインのような既知の領域が現れるでしょう。機能的には機能しますが、まだ改善の余地はあります。
それは現在の作業の失敗ではありません。それがパフォーマンスエンジニアリングの本質です。基準が動き、顧客データ量が増加し、「十分速い」という定義も変化します。この最適化の価値は、節約されたミリ秒だけではありません。それは次のギャップを見つけて埋めるために確立されたプロセスです。
Ignite UIグリッドパフォーマンスの今後の展望
この投稿の最適化は、一つの集中したパフォーマンス作業のラウンドであり、このテーマの締めくくりではありません。すでにいくつかの分野が動き出しており、さらに多くの分野が積極的に探求されています。
すでに改善されたこと
仮想化のパフォーマンスは、本記事で扱ったソート、グループ化、フィルタリングの作業と並行して改善されています。行と列の仮想化は、大規模データセットのレンダリングを可能にする基盤です。これらの改善はデータパイプラインの向上と重なり合い、グリッドはデータの処理とレンダリングの両方で高速化しています。
まだ作業中のもの
日付解析は改善の余地がある分野として知られています。日付と時間列のソートやESFの結果は以前より劇的に改善されていますが、日付文字列の解析方法に起因する点で数字列よりは依然として遅いです。解析層でのより的確な作業が次の論理的なステップです。
バンドルのサイズは継続的な焦点です。必要以上に多くのJavaScriptを送る高速グリッドは、特に初期ロード時間がランタイムパフォーマンスと同じくらい重要なチームにとっては逆効果となります。電力網のフットプリントを減らしながら能力を犠牲にすることは、継続的なバランス調整です。
グリッドAPIの改良は並行して続けられています。これは直接的なパフォーマンスの問題ではありませんが、それに関連しています。よりクリーンなAPIは、性能に敏感なコードパスが意図しない形で呼び出される表面積を減らします。
レンダリングコスト、変更検出圧力、高頻度更新時のインタラクション応答性など、より広いランタイムパフォーマンスは依然として未解決の課題です。具体的な主張はありませんが、注目されています。
パフォーマンスに関するフィードバックを共有してください
パフォーマンスの向上は基準値と期待値を引き上げます。かつては遅かったものが速くなり、新たなボトルネックが現れます。
だからこそ、私たちは実際の使用状況からの有益なフィードバックを重視しています。本番環境でIgnite UIグリッドを使っていてパフォーマンスの問題に遭遇したら、GitHubで問題を開いてみてください。実際のシナリオや再現可能な事例は、次の改善の機会を特定するのに役立ちます。
締めくくり:パフォーマンスは要点ではなく約束として扱う
すべてのグリッドライブラリはパフォーマンスを機能としてリストアップしています。「数百万行を処理する」は、コミットメントではなくチェックボックスなどの他の機能とともに比較表に表示されます。
大規模なデータセットを扱うグリッドと、ユーザーが待たずに処理できるグリッドには違いがあります。その違いは機能リストには表示されません。ユーザーが列のヘッダーをクリックしたりフィルターダイアログを開いたりすると、即座に返信が来るか、UIがフリーズするのを見たときに表示されます。
この投稿の仕事はその区別に基づいています。マーケティング上の要件ではなく、実際の顧客が実際の性能壁にぶつかり、「うまくいく」と「速い」という主張は同じではないと認識することで生まれます。シュワルツ変換、反復グルーピングスタック、シングルパスESFパイプライン――どれも最初から明確ではなく、動作が保証されておらず、すべてを正当化するために測定が必要でした。
パフォーマンスは、すぐに受け入れてすぐに進む機能ではありません。これらのコンポーネントに依存している開発者やエンドユーザーに対して、UIが邪魔をすることなく、実際の作業を大規模に行う継続的な義務です。
私たちはこれからも達成し続けるつもりです。