UPDATE@2016-01-26
MagJS のパフォーマンスを再度検証した結果、現時点では本番に投入できるレベルではないことが判明しました。
TL;DR
window.requestAnimationFrame
でゴリっと描画してます
はじめに
スーパークレイジーに速い MagJS はどうやってその速度を実現しているのか。さらっとコードを読んでみてわかったことをメモ。
描画ロジック
まずは再描画ロジックを追ってみます。
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
を覗いてみます。
/**
* 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 の説明を読んでみます。
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.
仕組み
FastDom は アプリ/ライブラリと DOM の間で調整役となる。DOM へのアクセスをまとめることで不要なリフローを避け、レイアウト速度を劇的に向上させることができる。
読み書きはそれぞれ専用のキューに溜められる。window.requestAnimationFrame が次のフレームを描画する時に、キューは処理され空になる(まず読み込みキュー、次に書き込みキューの順)。
requestAnimationFrame
は知らなかったのですが、どうやら効率的にアニメーションを実装するための関数のようです。fastdom では、requestAnimationFrame
が使えない場合には window.setTimeout
で代用しています。なので、この部分に関しては一応、古いブラウザでも動かないということはなさそうです。
描画関数の中身
元に戻って、makeRedrawFun
を追ってみます。いろいろやってますが、描画に絡む重要な部分はココ。
//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
に定義されています。このファイルの先頭部分をみると、またもやよそのライブラリからポーティグしています。
/*
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 には以下の断り書きが。。。
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 という別のライブラリの fork です。
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 のタグをサポートしています。
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
一発によるアニメーションはサポートされるのか。
仕様にはそのあたりの詳細が書いていないので、試しに jsFiddle で以下の 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 に挿入できるような仕掛けを用意する方向に行かざるを得ないんでしょうね。
なかなか難しいものです。