haml との互換性にはかなり気を使っているけど、一部意図的に非互換にしていたり、正確な仕様がわからず再現できていない箇所があったり、haml の奇妙な挙動が直っていたりして、完全に全く同じ動作にはなっていない。
先日 faml を本番に投入して今も動いている。faml 導入にあたって実際にはアプリケーション側のビューを一部書き換えたけど、大量のビューがある中での変更点は十分少なかったと思う。
なお、このエントリ内での「元々の haml」は haml 4.0.6 を指している。過去のバージョンは知らない。
どれくらい高速なのか
元々 faml を書き始めたきっかけは「haml と slim に文法的に大きな差があるわけではないんだし、slim と同程度高速な haml のレンダリングエンジンは書けるはず」という点だった。 なので、slim と同じくらい高速ではあるものの、slim 以上に高速というわけではない。ただ、faml は一部 C 拡張を利用しているので slim よりも若干高速なケースは存在する。
マイクロベンチは faml のリポジトリ内で rake benchmark
すれば実行できる。
travis-ci の after_script で rake benchmark
を実行しているので、そこで結果を見ることができる。
例: https://travis-ci.org/eagletmt/faml/jobs/57138591
なぜ高速なのか
まず slim と同じバックエンドの temple を使っている。 これはシンプルながら強力なライブラリで、temple が定めた S 式にコンパイルすると temple がそれを Ruby のコードに変換してくれ、更に S 式を変換するフィルタを間に挟むことができる。 このフィルタにより最適化を行ったり、独自の文法を導入しつつそのコンパイラを挟んだりできる。
また、属性の描画を高速化するためになるべく静的にコンパイルするようにしている。
例えば %input{checked: false}
とあったらリテラルしかないので静的に <input>
という文字列まで作ることができる。
これを達成するには Ruby のコードを解釈する必要があるので、そのためのライブラリとして parser を使っている。
Ruby のパーサとしては標準ライブラリの ripper や ruby_parser があるけど、僕は parser が一番使いやすいと思っている。
各 AST ノードに場所の情報がちゃんと入っていたり、その場所に書いてあったコード片を文字列として取り出せるのがいい。
属性を静的にコンパイルできなかった場合は実行時に描画することになるが、この部分で C 拡張を使っている。 これは slim には無い特徴なので、動的な属性が多く存在するようなページでは slim よりも若干高速になるかもしれない。
実際に Web アプリケーションが高速になるのか
完全にアプリケーションによると思っていて、1ページ内で多くのビューをレンダリングしていて haml が支配的である場合も、そうでない場合もある。 多くの場合、スロークエリを改善したり、適切にキャッシュを導入したり、アルゴリズムを改善するほうがずっと効果がありそう。 ただ、faml は haml と高い互換性を維持しているので、アプリケーションコードを変更せずとりあえず faml に入れ替えてみるだけで、一部のページで効果があるかもしれない。この点は魅力的だと思う。
非互換な部分は何なのか
属性に Hash を渡したときの挙動
元々の haml では %span{foo: {bar: 'baz'}}
は <span foo-bar='baz'></span>
となるが、faml では <span foo='{:bar=>"baz"}'></span>
となる。
ただし、%span{data: {bar: 'baz'}}
は <span data-bar='baz'></span>
となる。
このように、Hash を渡したときは data 属性の場合に限り hyphenate され、それ以外は単に to_s
される。
高速化のために意図的にこのような非互換性を導入している。
Hash の hyphenate は属性の描画の中では比較的重いものなので、なるべく to_s
だけで済ませたい。
この挙動は data 属性のときにしか普通使わないはずだと思い、data 属性を特別扱いしている。
ちなみに、一部の属性を特別扱いするのは元の haml にも存在して、id
と class
が該当する。
id
に Array を渡すと join("_")
した値がセットされ、class
に Array を渡すと join(" ")
した値がセットされる。
class
のほうの挙動を知ってる人は多そうだけど、id
のほうは知らなかった人も結構いるんじゃないかと思う。
常時自動 html エスケープ
元々の haml では Rails プロジェクトでなければデフォルトでは自動 html エスケープが無効になっている。
たまに sinatra を使ったときにはまったりするので、faml では自動 html エスケープを常に有効化している。デフォルトを無効にする手段は提供していない。
Rails プロジェクト以外で html エスケープを無効化したい箇所では !=
等の文法を使う。
ugly モードのみ
元々の haml では html をインデントするかどうかを示す ugly というフラグがあったけど、faml では常に ugly、つまりインデントを行わない。
Haml::Helpers 無し
succeed や surround といった haml 固有のヘルパメソッドが存在していたけど、faml では preserve 以外は提供していない。 preserve は使い所がまれに存在するけど、それ以外は無くても困らないと思って外した。 もし他に本当に必要なヘルパメソッドがあったら issue を立てて教えてください。
Object reference 無し
この機能を使ってる人いるんだろうか…… http://haml.info/docs/yardoc/file.REFERENCE.html#object_reference_
whitespace removal の仕様
haml には >
と <
によって空白を取り除く文法があって、これの詳細な仕様が不明で再現しきれてない。
もし明らかに間違っているものを見つけたら issue を立てて教えてください。
元々の haml 側の挙動も怪しくて、例えば
%div< hello world %div< #{'hello'} world
を render するとどうなるかというと、
<div>hello world</div> <div>helloworld</div>
になる。faml は前者の挙動が正しいと判断し、
<div>hello world</div> <div>hello world</div>
となるようにしている。
haml のバグ、あるいは一貫性の無い仕様
plain フィルタに interpolation が含まれていたときの空行
whitespace removal で触れたケースに似てるけど、
:plain hello world %br :plain #{'hello'} world %br
を ugly mode で render すると、元々の haml では
hello world <br> hello world <br>
となって、謎の改行が入る。faml ではこれも haml のバグだと判断し、
hello world <br> hello world <br>
となるようにしている。 ちなみに markdown フィルダでも同様の謎の改行が存在する。
空の plain フィルタでの空行
また、同じく :plain
において、:plain
の中身が空だったときに空行が入るかどうかに差がある。
%br :plain %br
これを元々の haml で render すると
<br> <br>
になる一方、faml では
<br> <br>
になる。
! または & で始まるテキスト
haml において、行頭の !
または &
には意味があり、それぞれ html エスケープをしない、する、という意味を持つ。
!=
ではなく !
だけの場合は、その後にくるテキスト内の interpolation において html エスケープをするかしないかを制御する。
%span!hello !hello %span! hello ! hello
を元々の haml が render すると
<span>hello</span> !hello <span>hello</span> hello
となり、タグ付きの場合は !
の後に空白がなくても !
が haml の記号として解釈される一方、タグ無しのテキストの場合は !
の後に空白がないと haml の記号として解釈されない。
faml では
<span>hello</span> hello <span>hello</span> hello
となり、どちらの場合も行頭の !
を haml の記号として解釈する。&
の場合も同様。
!
や &
で始まるテキストを書く場合は、行頭を ==
または \
でエスケープする必要がある。
まとめ
既に haml から slim に移行してしまったり、最近だとクライアントサイド JS での描画やモバイルアプリケーションがメインになってサーバサイドは単なる JSON API サーバになってるところも多そうだけど、もし haml が好きで使ってるけど遅いのが気になっている人がいれば是非 faml を試してみてほしいです。