調査：MagJS の速さの理由

# UPDATE@2016-01-26

MagJS のパフォーマンスを再度検証した結果、現時点では本番に投入できるレベルではないことが判明しました。

- [検証：MagJS の本当の描画パフォーマンス](http://qiita.com/t-suwa/items/dac3c929399185d2697f)

# TL;DR

`window.requestAnimationFrame` でゴリっと描画してます

# はじめに

スーパークレイジーに速い MagJS はどうやってその速度を実現しているのか。さらっとコードを読んでみてわかったことをメモ。

# 描画ロジック

まずは再描画ロジックを追ってみます。

```js:mag.jsから抜粋
  mag.redraw = function(node, idInstance, force) {
    if (!pendingRequests[idInstance]) {

      if (!node || typeof idInstance == 'undefined') {
        throw Error('Mag.JS - Id or node invalid: ' + idInstance);
      }

      // verify idInstance
      if (!isValidId(node.id, idInstance)) {
        return
      }

      // clear existing configs ?
      // TODO: per idInstance / id ?
      if (force) mag.fill.configs.splice(0, mag.fill.configs.length)

      if (force) mag.mod.clear(idInstance)

      var fun = makeRedrawFun(node, idInstance, force)

      // check for existing frame id then clear it if exists
      fastdom.clear(mag.mod.getFrameId(idInstance))
        //ENQUEUE
      var fid = fastdom.write(fun);
      //save frame id with the instance 
      mag.mod.setFrameId(idInstance, fid)
        // then if instance already has frame id create new discard old or just retain old
    }
  }
```

`pendingRequests` は何度も再描画が呼ばれないようにするための仕掛けです。Mithril では再描画を抑制するための仕組みとして内部カウンタを利用しており `m.startComputation` でインクリメントし、`m.endComputation` でデクリメントし、内部カウンタが 0 になったタイミングで再描画が走るようになっています。要するに参照カウンタと同じようなものです。

MagJS でもこの仕組みを拝借しており、`mag.begin` と `mag.end` で `pendingRequests` が増減されます。

続けて描画コードを見ると、エラーチェックの後、`var fun = makeRedrawFun(node, idInstance, force);` で描画用の関数を作成しています。この関数は、`fastdom.write` に渡されています。

# fastdom

早速、`fastdom.js` を覗いてみます。

```js:fastdom.js先頭部分
/**
 * FastDom
 *
 * Eliminates layout thrashing
 * by batching DOM read/write
 * interactions.
 *
 * @author Wilson Page <wilsonpage@me.com>
 */

;(function(fastdom){

  'use strict';

  // Normalize rAF
  var raf = window.requestAnimationFrame
    || window.webkitRequestAnimationFrame
    || window.mozRequestAnimationFrame
    || window.msRequestAnimationFrame
    || function(cb) { return window.setTimeout(cb, 1000 / 60); };
```

コメントから、どうやら DOM の読み書きをまとめて行うライブラリだとわかりますが、これをまんまコピっている模様。本家 [fastdom](https://github.com/wilsonpage/fastdom) の説明を読んでみます。

```text:原文
How it works

FastDom works as a regulatory layer between your app/library and the DOM. By batching DOM access we avoid unnecessary document reflows and speed up layout perfomance dramatically.

Each read/write job is added to a corresponding read/write queue. The queues are emptied (reads, then writes) at the turn of the next frame using window.requestAnimationFrame.
```
```text:意訳
仕組み

FastDom は アプリ/ライブラリと DOM の間で調整役となる。DOM へのアクセスをまとめることで不要なリフローを避け、レイアウト速度を劇的に向上させることができる。

読み書きはそれぞれ専用のキューに溜められる。window.requestAnimationFrame が次のフレームを描画する時に、キューは処理され空になる（まず読み込みキュー、次に書き込みキューの順）。
```

`requestAnimationFrame` は知らなかったのですが、どうやら効率的にアニメーションを実装するための関数のようです。fastdom では、`requestAnimationFrame` が使えない場合には `window.setTimeout` で代用しています。なので、この部分に関しては一応、古いブラウザでも動かないということはなさそうです。

# 描画関数の中身

元に戻って、`makeRedrawFun` を追ってみます。いろいろやってますが、描画に絡む重要な部分はココ。

```js:mag.jsから抜粋
      //RUN VIEW FUN
      mag.mod.callView(node, idInstance);

      //START DOM
      mag.fill.setId(node.id)
      mag.fill.run(node, state)
      // END DOM
```

`callView` はユーザー定義の `view` 関数の呼び出しです。ここで、`state.h2 = {...}` みたいな書き換えが発生します。その結果を反映するのが、`mag.fill.run` です。

# mag.fill.run

この関数は `fill.js` に定義されています。このファイルの先頭部分をみると、またもやよそのライブラリからポーティグしています。

```js:mag.js先頭部分
/*
MagJS v0.21
http://github.com/magnumjs/mag.js
(c) Michael Glazer
License: MIT
Originally ported from: https://github.com/profit-strategies/fill/blob/master/src/fill.js
*/
```

本家 [fill](https://github.com/profit-strategies/fill/blob/master/src/fill.js) には以下の断り書きが。。。

```text:
NOT READY FOR PRODUCTION

This project is under heavy development, and the API has not solidfied yet. Don't use this for anything important...yet.
```
**本番で使っちゃダメ**って書いてありますね ;-) なお、`fill.js` も実は [Transparency](https://github.com/leonidas/transparency) という別のライブラリの fork です。

```text
Transparency is a semantic template engine for the browser. It maps JSON objects to DOM elements by id, class and data-bind attributes.
```

Transparency では id、class、専用の data-bind 属性のいずれかを使って、JSON を DOM にマッピングできます。fill.js はこれに加えて、各種属性の設定と HTML のタグをサポートしています。

```text
Origins and Alternatives

This project was forked from the very impressive Transparency project to attempt the following:

    allow the setting of attributes (without needing to use directives)
        eventually phasing out directives completely
    add support for manipulating strings of HTML (without jsdom)
        for superfast rendering of html on server (running node.js)
```

MagJS では、これをさらに拡張しています。ざっくり言うと、再帰的に DOM の探索を行って、マッチした要素にデータをはめ込むのが `mag.fill.run` の仕事です。

# わかったことと素朴な疑問

MagJS は生 DOM をベタベタさわりながらも、`requestAnimationFrame` で描画スピードを稼いでいた、ということがわかりました。ぱっと見では差分検出で描画を最小化しているような箇所は見当たりませんでした。その分、データの当て込み処理は再帰を使って素直にシンプルに富豪的に実装できます。なかなか興味深いというか、ちょっとずるいよねぇという印象です。

でも、`window.requestAnimationFrame` を使うことの問題点はないのか。まず思いついたのは CSS のアニメーションです。つまり、自力で毎フレーム描画するのではなく、`transition` 一発によるアニメーションはサポートされるのか。

[仕様](http://www.w3.org/TR/html51/webappapis.html#animation-frames)にはそのあたりの詳細が書いていないので、試しに [jsFiddle](http://jsfiddle.net/Ledsjt1m/5/) で以下の 4 パターンを比較テスト。

- CSS transition
- requestAnimationFrame + CSS transition
- requestAnimationFrame + manual animation
- setTimeout animation

結果、手元の IE/Chrome/FireFox では `requestAnimationFrame` で CSS アニメーションがサポートされているのを確認できました。これをもって本当に大丈夫とは言い切れませんが、ひとまず、CSS の transition と script アニメーションは別物という理解で良さそうです。

# まとめ

MagJS の速さの理由を探りました。fastdom は他のライブラリとの連携が簡単にできるので、お手軽にスピードアップするには良さそうです。

一方で、MagJS は静的な html にデータを当て込む仕組みなので、本格的な SPA を構築しようとすると、各ページで必要となる html を `<div id="page1" style="display: none;">...</div>` みたいな形でモリモリに盛り込んだモノリシックな html が必要になりそうな予感があります。

でもそれはいつか必ず破綻するでしょう。

おそらく、ページごとに分割した html を動的にロードしてベースの html に挿入できるような仕掛けを用意する方向に行かざるを得ないんでしょうね。

なかなか難しいものです。