がんばってmxGraphの初期表示パフォーマンスを改善する

### どうしてこの記事を書くことになったのか

mxGraphでノード数が数千になるようなグラフの描画をすることになったとき、初期表示がとんでもなく遅くなってしまい、小手先のオプションでは歯が立たなかったので、ライブラリを見ていじったときの感想をまとめました。
なお、ここでいうパフォーマンス改善は、2分を30秒にする～完全にフリーズするのを避けるくらいの改善です。
※用語があやふやなので、誤記や意図不明などありましたらコメントお願いします。

## やること

1. 影響のないメソッドをオーバーライドして無くす
2. 不要なループを削除する

## 手段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以降は後回しにすることにしました。


```javascript:javascript：変更後のコード
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`も走っていることかと思いますので、そちらも似た形で変更します。

```javascript:javascript：変更後のコード
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以外の方は適当に読み替えてください。）

```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);`が取れなくなるので、これは先にやっておきます）

```typescript:typescript
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のリファレンス](https://jgraph.github.io/mxgraph/docs/js-api/files/view/mxGraph-js.html)
    - 変数やAPIなど全部まとまっていますが、あまりにも多すぎるので、頑張ってください…
- [GitHub - jgraph/mxgraph](https://github.com/jgraph/mxgraph)
    - リファレンスにはコードまで載っていないので、こちらで確認してください。手元のコード見るよりも見やすいと思います。


[^1]: ツリーレイアウトは一番上のノードから順番に位置決めをしていきます。ので、分岐しないツリーでなければ、ノードが増えるごとに上位ノードの再計算が必要になり、その都合かなと思っています。ノードを左上から順に固定し、一番上の子ノード、子ノードがなければ兄弟ノードを描画するようにすれば親ノードをさかのぼらなくてもいいんですけどね…（そういうオプションがないかと思って探しましたが、管見の限りありませんでした）
[^2]: 普通にノードを追加するときにも使うので、一個ずつ描画しようとするのは当然と言えば当然ですが…
[^3]: parentはツリー全体のトップノードになります。どこかで指定しているはずなので、それを使います。