GrapeCity さんが誇る高機能コンポーネント群・Wijmo。
とっても便利ですが、その中の FlexGrid は上手に使わないと簡単にパフォーマンスが低下してしまうので注意が必要です。
はじめに
Web アプリを開発する際、GrapeCity 謹製の Wijmo という製品のお世話になることがあります。
特に Excel ライクなデータグリッドビューである FlexGrid は非常に強力で、ビジネスアプリケーションでしばしば汎用されます。
けっこうお高いので個人開発では手が出せませんが、React や Angular、Vue といった主要な JS フレームワークとの相性も良く、頻繁に機能改善も行われているため、個人的には予算を割いてでも購入しておきたいと思える製品です。
2018年には Visual Studio Code で利用できる GUI ツール「Wijmo Designer」も登場し、より直感的にグリッドを構築できる環境が充実しつつあります。
ただ、そんな FlexGrid ですが、意外と初心者向けの体系的な情報が少なく、実際に製品を組み込んでアプリケーションを開発している現場でプログラマが悪戦苦闘している様子をよく目にします。
軽量なはずの FlexGrid の意外な弱点
ところで、この動画を見てくれ。こいつをどう思う?
久々に #wijmo ネタ。
— たーせる (@tercel_s) January 11, 2020
Angular に WjFlexGrid を組み込んだときのパフォーマンスについて。
矢印キーや Tab、Enter などでグリッドのセル移動をするたびに、バインドされているデータの中身はまったく更新されていないにも関わらず Change Detection が無駄打ちされている。 ぜひ改善してほしいなぁ。 pic.twitter.com/Iq6GPg2Xrj
すごく……(動きが)カクカクです。
本来であれば、隣接するセルからセルへとスムーズにフォーカスが移る動きが理想ですが、上記の動画ではいまいち処理が追い付いておらず、フォーカスがあちこちに飛んでしまっているように見えます。
動きもカクカクしており、思うようなパフォーマンスが出ていません。 この現象は、特に矢印キーを押しっぱなしにしてセル移動を試みた際に顕著になります。
原因その1
Angular で性能が悪化している場合、まずは Change Detection が無駄に走っていないかを疑うのが定石です。
<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 コンポーネントに width
や height
スタイルが未指定であったり、あるいは必要以上に巨大なサイズが指定されていたり、もしくは virtualizationThreshold プロパティの値が不適切だったりすることが多いと思います。
特に width
や height
を明示的に指定しないのは犯罪レベルの悪行になりますので注意しましょう。 実際に検証します。 以下に示すコードの通り、一方はサイズ指定なし、他方はサイズ指定ありで、その他の条件(バインドするデータやイベント等)は全く変えず、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)に抑えられていることが分かります。
子要素が増えれば増えるほど、グリッドのあらゆるイベントの処理コストが跳ね上がります。 ですので、極力 DOM 要素を抑えるための最適化の一環として、僅かな手間を惜しまず <wj-flex-grid>
に対してはサイズを明示的に指定するようにしましょう。
原因その3
今回は用いていませんが、itemFormatter や formatItem を用いてセルの外観(背景色や書体など)をカスタマイズすると、スクロール時の性能が悪化します。
セルの外観を変更したい場合は、cellFactory を用いると、パフォーマンスへの影響を最小限に抑えることができます。
GrapeCity の公式サンプルで、性能差を体感できます。 特にデフォルトレンダリング(最高速)と itemFormatter (低速)で、矢印キーを押しっぱなしにしてスクロールの体感速度を比較してみましょう。 itemFormatter の方は、途中でガクッとスクロールが閊えて動かなくなる現象を認めます。
これは、仮想セルが実体化される際に、コストの高いイベント処理が毎回走るせいです。
原因その4
グリッドにバインドしているデータ数が多すぎる……という可能性があります。
デフォルトでグリッドのセルは仮想化されていますし、余程のことがない限りデータ量がパフォーマンスのボトルネックになる恐れはないと考えますが、試す価値がありそうな解決策として 仮想スクロール対応が挙げられます。
ただし、この仮想スクロールを採用すると、ソートやフィルタがまともに動かなくなるので注意が必要です。
謎の技術で性能改善
とりあえず思い当たる原因を可能な限り潰した結果がこちらです。 ご査収ください。
謎の技術を使い、昨日のカーソル移動のガタつきが多少マシになった。
— たーせる (@tercel_s) January 12, 2020
これまで、セル移動の際に矢印キーを押し続けた場合、処理が追い付かずにフォーカスがガタガタだったけど、とりあえず縦方向の移動に関しては手触りが改善した。
ただ、相変わらず Change Detection は無駄打ちされている。 https://t.co/57QW0FJKhP pic.twitter.com/a2bLSY52NM
でも結局、パフォーマンスに大きな影響を与えている Change Detection は解決できませんでした。 これはもう製品の内部仕様なので、外部から侵襲できるものではないと判断しました。
無駄だと思いつつ、<wj-flex-grid>
をホストしているコンポーネントの ChangeDetectionStrategy をデフォルトから変えてみたり、セルの選択変更時のイベントを Angular の監視下から外そうと試みたりしましたが、僕のスキルがクソゴミカスハゲチンコなのでダメでした。
諦めた……。
というかこのために GrapeCity さまに年間 15 万円をお支払いしているので、ぜひ更なる軽量化を図っていただけると嬉しいなぁと思うたーせるなのでした(今日のわんこっぽく)。
おまけ1
ちなみに、現時点の最新版 (5.20193.637) を Angular に組み込んで使おうとすると、$ng build
でのコンパイル時に警告が出ます。 改善されますように(お願い)。
CSSのトランスパイルでワーニングが表示される - ワーニング・・・ https://t.co/FzKf3lZXiU
— たーせる (@tercel_s) January 11, 2020
おまけ2
さらにどうでもよいことですが、上記で紹介したカスタムセルファクトリーの公式サンプルはちょっとだけバグっています。
久々に#wijmoネタ。
— たーせる (@tercel_s) October 17, 2019
FlexGridのセル外観をカスタマイズするために、CustomCellFactoryなるものを試した。ItemFormatterプロパティやformatItemイベントより速い。
ただしGrapeCityの公式サンプルにはバグがあり、セル編集ができなくなっているが、まぁそれはご愛嬌だろう(-ωー,,)
この発言に対して GrapeCity の中の人(?)からお返事が。
サンプルではisReadOnlyがtrueに設定されていて編集不可かと。https://t.co/xoGsIOVs0F
— kuni (@UKGraphics) October 18, 2019
以下レスバトル。
実際に試すとわかりますが、このサンプルのCustomCellFactoryクラスをそのまま適用すると、FlexGridに対するisReadOnlyプロパティの設定如何にかかわらず、編集モードで値が変更することができません。
— たーせる (@tercel_s) October 18, 2019
詳細な情報提供ありがとうございます!こちら古いサンプルなので更新されないかもしれないのですが、内容を報告させていただきます。
— kuni (@UKGraphics) October 18, 2019
今日はここまでです。 またいつか Wijmo について取り上げる機会とモチベーションがあれば、カスタムエディタについて書きたいと思います。 ではでは。