どうしてこの記事を書くことになったのか
mxGraphでノード数が数千になるようなグラフの描画をすることになったとき、初期表示がとんでもなく遅くなってしまい、小手先のオプションでは歯が立たなかったので、ライブラリを見ていじったときの感想をまとめました。
なお、ここでいうパフォーマンス改善は、2分を30秒にする~完全にフリーズするのを避けるくらいの改善です。
※用語があやふやなので、誤記や意図不明などありましたらコメントお願いします。
やること
- 影響のないメソッドをオーバーライドして無くす
- 不要なループを削除する
手段1:影響のないメソッドをオーバーライドして無くす
グラフの描画をしているときのパフォーマンスをデベロッパーツールで取得すると、どのメソッドが動いているか分かります。動いているメソッドの中で、時間がかかっているもの、試行回数が多いものを中心に、カットできないか検討します。
最終的には消してみてバグがないか確認する必要がありますが、どのObjectに入っているかで、ある程度は消せるメソッドか否か分かります。
mxText
図形内にテキストを表示するものです。普通にグラフを書く場合は使うことになると思いますが、弊環境では外部オブジェクトとしてHTMLを追加し、そこにラベルなどを表示する方法をとっていたので、全く使っていませんでした。
私の環境では以下の一行で30~40%の処理時間軽減になりました。
mxText.prototype.updateBoundingBox = () => {};
以下は使用しない場合が多そうなので、削除ポイントです。
mxOutline
グラフの全体マップを小窓に表示する機能
mxCellOverlay
マウスオーバーなどでアイコンやツールチップを表示する機能
手段2:不要なループを減らす
似たような名前の処理を何度も行っているので私も完全には理解できていませんが、謎の繰り返しが多く発生しているので1、そこを削っていきます。変更を加えたのは2点です。
mxGraph.prototype.insertVertex
insertVertex
はノードをグラフに追加する処理です。これを行うと、insertVertex
→createVertex
→addCell
→addCells
→cellsAdded
と順に処理が走ってグラフが都度再描画されます。2 その後また次のノードをinsertVertex
で追加する、という流れです。
ですが、どう考えてもすべての図形を追加してから描画すればいい、ということで、addCell以降は後回しにすることにしました。
mxGraph.prototype.insertVertex = function(parent, id, value,
x, y, width, height, style, relative)
{
var vertex = this.createVertex(parent, id, value, x, y, width, height, style, relative);
cells.push(vertex);
targets.push(null);
sources.push(null);
//cells,targets,sourcesは配列。適当に作成しておいてください。
};
このあと任意の箇所でaddCells(cells,parent,null,sources,targets)
3をするだけです。(addCell
は飛ばします)
なお、insertVertex
の後にinsertEdge
も走っていることかと思いますので、そちらも似た形で変更します。
mxGraph.prototype.insertEdge = function(parent, id, value, source, target, style)
{
var edge = this.createEdge(parent, id, value, source, target, style);
cells.push(edge);
sources.push(source);
targets.push(target);
};
10%くらいの時間削減になりました。
mxCellRender.prototype.redraw
mxGraphでは、グラフコードの更新を行った後、endUpdate
を走らせて、実際にDOMに入れていく処理を呼んでいます。
その中のmxGraphView.validateCellState
の中で(おそらく)ノードのDOM内の表示位置(state)を決定していますが、この処理が走るたびDOMへの描画も行われてしまうので、すべてのノードの位置を確定させてからDOMに書き込むように、こちらも順番を変えていきます。
方法としては、rendering = false
でいったん処理→rendering = true
にしてから、mxCellRender.redraw
を走らせる形になります。
(メインのコードはtypescriptで書いていたので、typescript以外の方は適当に読み替えてください。)
setupGraph(): void {
//(略)
mxGraphView.prototype.rendering = false;
//(略)
}
...
draw(): void {
this.graph.view.rendering = true;
const cells = Object.values(this.graph.model.cells);
cells.forEach((cell: mxgraph.mxCell) => {
try {
const state = this.graph.view.getState(cell);
this.graph.cellRenderer.redraw(state, true, true);
} catch (e) {
console.log(e);
}
});
}
this.setupGraph();
this.drawTreeMethod();
this.draw();
また、redraw
を少しいじっておきます。(本当はredrawShape()
も後回しにしたいところですが、これを飛ばすとconst state = this.graph.view.getState(cell);
が取れなくなるので、これは先にやっておきます)
mx.mxCellRenderer.prototype.redraw = (state, force, rendering) => {
const shapeChanged = this.graph.cellRenderer.redrawShape(
state,
force,
rendering
);
if (rendering === true) {
if (state.shape != null && (rendering == null || rendering)) {
this.graph.cellRenderer.redrawLabel(state, shapeChanged);
this.graph.cellRenderer.redrawCellOverlays(state, shapeChanged);
this.graph.cellRenderer.redrawControl(state, shapeChanged);
}
}
};
こちらも10~20%くらいの時間削減になりました。
全部合わせれば読み込み時間を半分以下にするのも夢ではないです!
参考資料
-
mxGraphのリファレンス
- 変数やAPIなど全部まとまっていますが、あまりにも多すぎるので、頑張ってください…
-
GitHub - jgraph/mxgraph
- リファレンスにはコードまで載っていないので、こちらで確認してください。手元のコード見るよりも見やすいと思います。
-
ツリーレイアウトは一番上のノードから順番に位置決めをしていきます。ので、分岐しないツリーでなければ、ノードが増えるごとに上位ノードの再計算が必要になり、その都合かなと思っています。ノードを左上から順に固定し、一番上の子ノード、子ノードがなければ兄弟ノードを描画するようにすれば親ノードをさかのぼらなくてもいいんですけどね…(そういうオプションがないかと思って探しましたが、管見の限りありませんでした) ↩
-
普通にノードを追加するときにも使うので、一個ずつ描画しようとするのは当然と言えば当然ですが… ↩
-
parentはツリー全体のトップノードになります。どこかで指定しているはずなので、それを使います。 ↩