Wijmo の FlexGrid を使う際に気を付けるべきこと(パフォーマンスについて)

GrapeCity さんが誇る高機能コンポーネント群・Wijmoウィジモ

とっても便利ですが、その中の FlexGrid は上手に使わないと簡単にパフォーマンスが低下してしまうので注意が必要です。

はじめに

Web アプリを開発する際、GrapeCity 謹製の Wijmo という製品のお世話になることがあります。

特に Excel ライクなデータグリッドビューである FlexGrid は非常に強力で、ビジネスアプリケーションでしばしば汎用されます。

けっこうお高いので個人開発では手が出せませんが、React や Angular、Vue といった主要な JS フレームワークとの相性も良く、頻繁に機能改善も行われているため、個人的には予算を割いてでも購入しておきたいと思える製品です。

2018年には Visual Studio Code で利用できる GUI ツール「Wijmo Designer」も登場し、より直感的にグリッドを構築できる環境が充実しつつあります。

ただ、そんな FlexGrid ですが、意外と初心者向けの体系的な情報が少なく、実際に製品を組み込んでアプリケーションを開発している現場でプログラマが悪戦苦闘している様子をよく目にします。

軽量なはずの FlexGrid の意外な弱点

ところで、この動画を見てくれ。こいつをどう思う?

すごく……(動きが)カクカクです。

本来であれば、隣接するセルからセルへとスムーズにフォーカスが移る動きが理想ですが、上記の動画ではいまいち処理が追い付いておらず、フォーカスがあちこちに飛んでしまっているように見えます。

動きもカクカクしており、思うようなパフォーマンスが出ていません。 この現象は、特に矢印キーを押しっぱなしにしてセル移動を試みた際に顕著になります。



原因その1

Angular で性能が悪化している場合、まずは Change Detection が無駄に走っていないかを疑うのが定石です。

qiita.com

<wj-flex-grid> をホストしているコンポーネントクラスのコンストラクタに以下のコードを埋め込んで、開発者ツールのコンソールを確認してみましょう。

  constructor(zone: NgZone) {
    zone.onMicrotaskEmpty.subscribe(() => console.log('detect change'));
  }

すると、1 回のフォーカス移動だけで Change Detection が複数回走っていることが判りました。

矢印キーを押しっぱなしにすると、あっという間にイベントがふくそうして、処理落ちの原因になってしまいます。

ちなみに API ドキュメントによると FlexGrid は内部的に CollectionView というメンバを持っており、選択状態にあるアイテムは currentItem プロパティで保持しているようです。 セルのフォーカスを移動させた際に、ビューとプロパティを同期させるために Change Detection が走るのは仕方ないとしても、これはさすがに無駄打ちだろうと思うわけです。

原因その2

続いて、グリッドコンポーネント配下の DOM ツリーが無駄に肥大化していないかを疑います。

DOM 要素として実体化されているセル数は、wjFlexGrid.hostElement.querySelectorAll('.wj-cell').length で確認できます。 このセル数が増えると、覿てきめんに性能が落ちます。

原因としては、FlexGrid コンポーネントwidthheight スタイルが未指定であったり、あるいは必要以上に巨大なサイズが指定されていたり、もしくは virtualizationThreshold プロパティの値が不適切だったりすることが多いと思います。

特に widthheight を明示的に指定しないのは犯罪レベルの悪行になりますので注意しましょう。 実際に検証します。 以下に示すコードの通り、一方はサイズ指定なし、他方はサイズ指定ありで、その他の条件(バインドするデータやイベント等)は全く変えず、DOM 要素の数をコンソールに吐いてみました。

<!-- サイズ指定なし -->
<div style="width: 100vw; height:100vh;">
  <wj-flex-grid 
  (initialized)="initializeGrid(grid)"
  [itemsSource]="data" #grid>
  </wj-flex-grid>
</div>
<!-- サイズ指定あり -->
<div style="width: 100vw; height:100vh;">
  <wj-flex-grid 
  (initialized)="initializeGrid(grid)"
  style="width:100%; height:100%;" [itemsSource]="data" #grid>
  </wj-flex-grid>
</div>

すると、一見グリッドの見た目は全く同じであるにもかかわらず、サイズ指定なしの場合は 816 ものセルが実体化されているのに対し、サイズ指定ありの場合は 208 (約 1/4)に抑えられていることが分かります。

f:id:tercel_s:20200113123819p:plainf:id:tercel_s:20200113123809p:plain
wj-flex-grid に対するサイズ指定なし(左)とあり(右)の比較

子要素が増えれば増えるほど、グリッドのあらゆるイベントの処理コストが跳ね上がります。 ですので、極力 DOM 要素を抑えるための最適化の一環として、僅かな手間を惜しまず <wj-flex-grid> に対してはサイズを明示的に指定するようにしましょう。

原因その3

今回は用いていませんが、itemFormatterformatItem を用いてセルの外観(背景色や書体など)をカスタマイズすると、スクロール時の性能が悪化します。

セルの外観を変更したい場合は、cellFactory を用いると、パフォーマンスへの影響を最小限に抑えることができます。

GrapeCity の公式サンプルで、性能差を体感できます。 特にデフォルトレンダリング(最高速)と itemFormatter (低速)で、矢印キーを押しっぱなしにしてスクロールの体感速度を比較してみましょう。 itemFormatter の方は、途中でガクッとスクロールがつかえて動かなくなる現象を認めます。

これは、仮想セルが実体化される際に、コストの高いイベント処理が毎回走るせいです。

原因その4

グリッドにバインドしているデータ数が多すぎる……という可能性があります。

デフォルトでグリッドのセルは仮想化されていますし、余程のことがない限りデータ量がパフォーマンスのボトルネックになる恐れはないと考えますが、試す価値がありそうな解決策として 仮想スクロール対応が挙げられます。

ただし、この仮想スクロールを採用すると、ソートフィルタがまともに動かなくなるので注意が必要です。



謎の技術で性能改善

とりあえず思い当たる原因を可能な限り潰した結果がこちらです。 ご査収ください。

でも結局、パフォーマンスに大きな影響を与えている Change Detection は解決できませんでした。 これはもう製品の内部仕様なので、外部から侵襲できるものではないと判断しました。

無駄だと思いつつ、<wj-flex-grid>をホストしているコンポーネントChangeDetectionStrategy をデフォルトから変えてみたり、セルの選択変更時のイベントを Angular の監視下から外そうと試みたりしましたが、僕のスキルがクソゴミカスハゲチンコなのでダメでした。

諦めた……。

というかこのために GrapeCity さまに年間 15 万円をお支払いしているので、ぜひ更なる軽量化を図っていただけると嬉しいなぁと思うたーせるなのでした(今日のわんこっぽく)。



おまけ1

ちなみに、現時点の最新版 (5.20193.637) を Angular に組み込んで使おうとすると、$ng build でのコンパイル時に警告が出ます。 改善されますように(お願い)。


おまけ2

さらにどうでもよいことですが、上記で紹介したカスタムセルファクトリーの公式サンプルはちょっとだけバグっています

この発言に対して GrapeCity の中の人(?)からお返事が。

以下レスバトル。

今日はここまでです。 またいつか Wijmo について取り上げる機会とモチベーションがあれば、カスタムエディタについて書きたいと思います。 ではでは。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.