SeaPig の HTML プレビューの更新に Mithril を利用したら高速になった話

![SeaPig](https://raw.githubusercontent.com/yasumichi/seapig/master/seapig.png)[SeaPig](https://github.com/yasumichi/seapig) is converter from markdown to html with marked.js and highlight.js.

３年ぶりに [SeaPig 0.8.3](https://github.com/yasumichi/seapig/releases/tag/v0.8.3) をリリースしました。 :sweat_smile:

至極当然の話ですが、DOM を直接操作せずに、Virtual DOM を使用するフレームワーク [Mithril](https://mithril.js.org/) を通して操作するようにしたら、再描画が早くなったというお話です。

# まずは SeaPig の概要を

[SeaPig](https://github.com/yasumichi/seapig) は、Qiita の投稿にも使用する Markdown と呼ばれる記法で左ペインのエディタ部分に入力すると右ペインのブラウザー部分に HTML で表示してくれるコンバーターです。

![](https://raw.githubusercontent.com/yasumichi/seapig/master/images/seapig.png)

変換した HTML を保存できるのは当然のこと、PDF として出力することもできます。[GitHub Flavored Markdown](https://github.github.com/gfm/) 記法を使える他に以下の JavaScript ライブラリーを利用して、図を描画することも可能です。

- [mermaid](https://knsv.github.io/mermaid/)(flowchart、sequence、gantt が利用可能。Pie chart は未対応。)
- [viz.js](https://github.com/mdaines/viz.js)
- [uiflow](https://github.com/hirokidaichi/uiflow)(use [forked version](https://github.com/tkrkt/uiflow#fix-argument-in-compile))

Markdown から HTML への変換には、[marked](https://github.com/chjj/marked) を利用しており、コードブロックにおいては、[highlight.js](https://highlightjs.org/) による簡単な Sytax ハイライトを付与することができます。

# これまでのプレビューの再描画

Editor で改行などが行われたときにプレビューを担当するプロセス [seapig/js/preview.js](https://github.com/yasumichi/seapig/blob/v0.8.0/js/preview.js) にエディターのデータを送信し、[marked](https://github.com/chjj/marked) で変換した結果を body の innerHTML にそのまま突っ込んでおりました。

当然、body 全体の再描画が走るため、大きなファイルになるほど、再描画に時間がかかりました。（なので1文字入力ではなく改行をトリガーにしていました。）

```javascript
    /**
     * Refresh preview
     * @param {object} event
     * @param {string} data - markdown text
     * @param {string} baseURI
     * @listens preview
     */
    ipcRenderer.on('preview', (event, data, baseURI) => {
      let base = document.getElementsByTagName("base")[FIRST_IDX];
      if (baseURI != "") {
        base.setAttribute("href", baseURI);
      }
      base.setAttribute("target", "_blank");
      document.getElementById('body').innerHTML = md2html.convert(data);

      // process task list items
      var listitems = document.getElementsByTagName("li");
      for(var i=0; i<listitems.length; i++) {
        var fchild = listitems[i].firstElementChild;
        if(fchild != null && fchild.nodeName === "INPUT" && fchild.type === "checkbox") {
          listitems[i].classList.add("task-list-item");
        }
      }
```

:sweat_smile: `document.getElementById('body')` なんてしなくても `document.body` で行けたよね、というのもあったり…。

# Mithril を活用したプレビューの再描画

Editor での変更をトリガーにプレビューを担当するプロセス [seapig/js/preview.js](https://github.com/yasumichi/seapig/blob/v0.8.2/js/preview.js) にエディターのデータを送信し、[marked](https://github.com/chjj/marked) で変換した結果を [m.trust()](https://mithril.js.org/trust.html) で受け取り、[m.render()](https://mithril.js.org/render.html) で body に設定します。これにより、変更のあった部分のみ DOM の再構築が行われるようになり、高速になりました。

```javascript
    /**
     * Refresh preview
     * @param {object} event
     * @param {string} data - markdown text
     * @param {string} baseURI
     * @listens preview
     */
    ipcRenderer.on('preview', (event, data, baseURI) => {
      let base = document.getElementsByTagName("base")[FIRST_IDX];
      if (baseURI != "") {
        base.setAttribute("href", baseURI);
      }
      base.setAttribute("target", "_blank");

      // render body
      m.render(document.body, m.trust(md2html.convert(data)));

      // process task list items
      var listitems = document.getElementsByTagName("li");
      for(var i=0; i<listitems.length; i++) {
        var fchild = listitems[i].firstElementChild;
        if(fchild != null && fchild.nodeName === "INPUT" && fchild.type === "checkbox") {
          listitems[i].classList.add("task-list-item");
        }
      }
```

差分は、わずか、以下の通りです。

```diff
index 625fb1b..4af4792 100644
--- a/js/preview.js
+++ b/js/preview.js
@@ -30,6 +30,7 @@
     const {ipcRenderer} = require('electron');
     const fs = require('fs');
     const path = require('path');
+    const m = require("../external/mithril/mithril.min.js");
     const Md2Html = require('./md2html.js');
     const FIRST_IDX = 0;
     const NO_SCROLL = 0;
@@ -91,7 +92,9 @@
         base.setAttribute("href", baseURI);
       }
       base.setAttribute("target", "_blank");
-      document.getElementById('body').innerHTML = md2html.convert(data);
+
+      // render body
+      m.render(document.body, m.trust(md2html.convert(data)));

       // process task list items
       var listitems = document.getElementsByTagName("li");
```