Railsのテンプレートエンジンを、slimやhamlから、erbに戻して乗り換えた話

はじめに

OHEYAGOの開発をしている田渕です!

Railsのテンプレートエンジンには、デフォルトのerbや、代替のslimhamlなどがあります。

OHEYAGOではslimを採用していましたが、先日、テンプレートエンジンをslimからデフォルトのerbに変更しました。
細かい理由は後述しますが、OHEYAGOチーム内ではslimが技術的負債になっているという結論が出たからです。

また、自分が以前開発していた別のシステムでも、テンプレートエンジンをhamlからerbに変更したことがあります。
こちらも同様にhamlを負債だと考えたからです。

slimやhamlで開発しにくいという話はたまに聞きますが、実際にslimやhamlを削除するという話はネット上の記事でも殆ど見たことがないため、修正した方法を紹介します!

技術選定の理由

slimやhamlのメリットとデメリットを考えます。まずはメリットについてですが、

  • 書きやすく読みやすい文法(いずれのgithubリポジトリでもelegant syntaxという語句が使われています)
  • パフォーマンスが良い

というものが挙げられると思います。一方で、自分の考えるデメリットは以下の二点です。

  • 短く書けるが、そのことにメリットを感じない
  • デフォルトではない

結論から言うと、デメリットのほうが大きいと判断し、slimやhamlの採用を辞めました。

以下ではこれらの点について説明していきます。

パフォーマンスについて

まず、パフォーマンスについては、記事によってはerbが極端に遅く、slimやhamlが早いとされているものがあると思います。
しかし、そのような記事は、やや古い記事である可能性があります。

実は、Railsのerbの処理系は2017年頃(Rails5.1くらいの時期)に、erubisからerubiに置き換わっています。(こちらが該当のPRです)

現状の速度比較は、hamlitというリポジトリがわかりやすく、READMEによると10%程度のパフォーマンス差のようです。

hamlit v2.8.1: 131048.9 i/s
erubi v1.6.0: 125445.4 i/s - 1.04x slower
slim v3.0.8: 121390.4 i/s - 1.08x slower

測定状況やバージョンによる差などもありそうですし、そもそもテンプレートエンジンの10%前後のパフォーマンスを気にする必要がないプロジェクトも多いのではないでしょうか。

文法について

確かに短くは書けますが、その分インデントや改行に文法上の意味があるため、改行が必要なRubyコードなどは非常に書きにくいと感じました。
Reactにカスタムデータ属性で値を渡したい場合の、slimとerbの記述例を紹介します。

まずはslimです。

#root *{ data: { \
  data1: @data1, \
  data2: @data2, \
  data3: @data3, \
  data4: @data4, \
  } }   

改行の前にバックスラッシュを書くなどの工夫が必要で、インデントの付け方にも制限がかかります。改行をしないと属性が多い場合には厳しいです。
一方、erbの場合は自然なRubyコードのように書くことができます。

<%= tag.div(
      id: 'root',
      data: {
        data1: @data1,
        data2: @data2,
        data3: @data3,
        data4: @data4
      }
    ) %>

確かに行数は増えますが、少なくともOHEYAGOのエンジニアチームでは、下のほうが書きやすく読みやすいと、全会一致しました。

また、OHEYAGOの場合はReactでTSXを書いているため、そちらとのコンテキストスイッチや、移行の際のコストも大きくなっていました。
erbであればコピペして数行の変更でTSXに変更できるけれど、slimなのでほとんど書き直さなければいけない、という状況に何度もぶつかりました。

デフォルトかどうか

もちろん、デフォルトが必ずしも良いわけではありませんが、slimやhamlのサポートが終わる可能性は、erbのサポートが終わる可能性よりはずっと高いだろうと考えています。
デフォルトから外れることは、将来の保守を考えた際に、それだけで大きなデメリットになり得ます。

以上のメリット・デメリットを比較した際に、先に移行コストを払っても移行してしまうことを決めました。
OHEYAGO以前に自分が携わったプロダクトでも、ほぼ同じ理由でhamlからerbの移行を決めました。

移行方法

実際にerbへと移行する方法について述べます。

hamlからerbへの移行

公式ではサポートしていませんが、herbalizerというツールがあるので、こちらを使って概ね変換が可能です。
例えば以下のようなシェルスクリプトhamlをerbに変換できます。

#!/bin/bash
set -ex

find ../app -name "*.haml" | while read line; do
  # need to download herbilizer
  ./herbalizer < $line > ${line/.haml/.html.erb}
done

実際にこの作業を行ったのが1年以上前の話なので、詳細は覚えていませんが、100%は対応できておらず、ある程度は手動で修正した記憶があります。

slimからerbへの移行

公式のサポートがあるようですが、自分が試した限りでは、Rubyのコードが多い場合には使い物になりませんでした。
例えば先程のslimのコードを変換しようとすると、以下のようになってしまいます。

<% _slim_splat_filter1 = {:hyphen_attrs=>["data", "aria"], :use_html_safe=>true, :sort_attrs=>true, :format=>:xhtml, :attr_quote=>"\"", :merge_attrs=>{"class"=>" "}, :default_tag=>"div"} %><div<% _slim_splat_filter2 = ::Slim::Splat::Builder.new(_slim_splat_filter1) %><% _slim_splat_filter3 = "root" %><% _slim_splat_filter2.attr("id", _slim_splat_filter3); _slim_splat_filter2.splat_attrs(({ data: { \ data1: @data1, \ data2: @data2, \ data3: @data3, \ data4: @data4, \ } })) %><%= _slim_splat_filter2.build_attrs %>> </div>

slimからhamlに変換するツールがあれば、haml経由でherbalizerを使ってerbに変換することも可能でしたが、hamlへの変換もできませんでした。

なので、もう全て手作業で変えるしかありません。
erbとslimが混ざっていても問題なく動かすことができる(slimファイルを消せばerbファイルを読みに行ってくれる)ので、一つずつ変えていきます。(スマートな解決策を求めていた方はごめんなさい!)

自動化したい場合は、herbalizerなどを参考にしながら自分でパーサコンビネータなどを書くしかなさそうです。
OHEYAGOでは画面数がそれほど多くなかったため、手動で終わらせてしまいました。

おわりに

slimもhamlも自分が導入したものではなく、チームに参加した時点で入っていたものですが、負債になる可能性が少しでもあり、わずかずつでもDX(開発体験)が低下しているのであれば、思い切って時間をかけて書き換えてしまいます。

それが許される社風やチームの空気があって、とてもありがたい限りです。
とても開発しやすい状態を作っていただいているので、それに応えて今後もその状態を維持するためにも、少しでも良いプロダクトを作っていこうと思います。