Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

最速のRubyテンプレートエンジンはどれ?ERB vs HAML vs Slim 徹底パフォーマンス比較

本記事では、Rubyで最も人気のある3つのテンプレートエンジン——標準搭載のERB、HAML、そしてSLIM——のパフォーマンスを実際に計測し、徹底的に分析します。

ベンチマークとは、自社のプロセスやパフォーマンス指標を業界最高水準や他社のベストプラクティスと比較する手法のことです。一方、ロードテスト(負荷テスト)は、システムに意図的に負荷をかけ、その応答を測定するプロセスを指します。

今回のゴールは、Rubyコードの実行時間を計測・レポートするためのメソッドを提供する「Benchmark」モジュールの使い方を学ぶことです。インラインテンプレートを作成してテストを実行し、3つのエンジンそれぞれから指標を抽出していきます。

その後、ロードテストに進みます。実際のアプリケーションで使うようなテンプレートビューを作成してサーバー上で公開し、「hey」というツールを使って負荷テストを実施します。heyは特定のWebアプリケーションへ負荷を送信し、各エンドポイント、ひいては各テンプレートエンジンのパフォーマンスに関する詳細なデータを取得できる優れたツールです。

セットアップ

まず最初に、当然ながらRubyがインストールされていることを確認しましょう。本記事の執筆時点での最新バージョンである2.7.0を使用します。加えて、Rails gemもインストールしてください。

RubyとRails gemがあれば、開始するための環境はほぼ整っています。このチュートリアルではIDEとしてVisual Studio Codeを使用しますが、お好みで別のエディタを選んでも問題ありません。

次に、Railsプロジェクト用のフォルダを選択し、以下のコマンドを実行します。

rails new haml-slim-erb

これにより、必要な依存関係がすべてダウンロードされ、Railsプロジェクトの雛形が作成されます。まずはプロジェクトの中身を一通り確認してみてください。

コードに進む前に、GemfileにSLIMとHAMLの依存関係を追加しておきましょう。

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platform: :mri
  gem 'haml'
  gem 'slim'
end

また、SQLiteのバージョンに関連するバグがデフォルトのプロジェクトに含まれています。Gemfile内の該当箇所を見つけて、以下のように変更してください。

gem 'sqlite3', '~> 1.3.0'

その後、bundle installコマンドを実行して依存関係をダウンロードします。

Benchmarkモジュールを探る

ここからは、testフォルダ内で直接作業を行います。フォルダを開くと、いくつかの空のフォルダが見えるはずです。そこに「benchmark」という名前の新しいフォルダと、example_1_test.rbexample_2_test.rbexample_3_test.rbという3つのファイルを作成しましょう。

Rubyでは、ファイル名が「test」で終わっている必要があります。そうすることでテストファイルとして認識されます。

続いて、最初のファイルに以下の内容を追加します。

require 'benchmark'

number = (0..50).to_a.sort{ rand() - 0.5 }[0..10000]

puts Benchmark.measure {
  20_000.times do
    number[rand()] * (0..50).to_a.sort{ rand() - 0.5 }[0..10000][rand()]
  end
}

1行目で必要なBenchmarkモジュールをインポートしている点に注目してください。次に、0〜50の範囲の数値からなるサイズ10,000のランダムな配列を生成しています。処理にある程度時間をかけるために、あえて大きな数値を使用しています。

measureメソッドは非常に便利で、Rubyコード内の任意の場所に配置でき、処理にかかる時間を計測できます。計測結果は戻り値として返され、putsによって出力されます。

ブロック内部では、ランダム生成した配列の実行を20,000回繰り返し、それぞれの値を掛け合わせています。

このテストファイルだけを個別に実行するには、以下のコマンドを発行します。

rake test TEST=test/benchmark/example_1_test.rb

結果は次のようになります。

0.702647   0.012353   0.715000 (  0.721910)

このレポートには、左から順にユーザーCPU時間、システムCPU時間、それらの合計、経過実時間が出力されます。時間の単位は秒です。

Benchmarkのその他のメソッドについては、後ほど実践の中で活用していきます。

インラインテンプレートのテスト

RubyのBenchmarkモジュールの仕組みを少し理解できたところで、今度は3つのテンプレートに対して実際にテストを実行してみましょう。

そのために、1つのテンプレートを作成し、それを3つのエンジンの構文に翻訳した上で、Benchmarkメソッドのもとで実行します。

2つ目のテストファイルに以下を追加してください。

require 'erb'
require 'haml'
require 'slim'
require 'benchmark'
require 'ostruct'

notes = OpenStruct.new title: 'Write an essay', description: 'My essay is about...', randomList: (0..50).to_a.sort{ rand() - 0.5 }[0..10000]

erb_example = <<-ERB_EXAMPLE
<span><%= notes.title %></span>
<span><%= notes.description %></span>
<table>
  <tr>
    <% notes.randomList.each do |note| %>
      <td><%= note %></td>
    <% end %>
  </tr>
</table>
ERB_EXAMPLE

slim_example = <<-SLIM_EXAMPLE
span= notes.title
span= notes.description
table
  tr
    - notes.randomList.each do |note|
      td= note
SLIM_EXAMPLE

haml_example = <<-HAML_EXAMPLE
%span= notes.title
%span= notes.description
%table
  %tr
    - notes.randomList.each do |note|
      %td= note
HAML_EXAMPLE

context = OpenStruct.new notes: notes
__result = ''

Benchmark.bmbm(20) do |bcmk|
  bcmk.report("erb_test") { (1..2000).each { ERB.new(erb_example, 0, '-', '__result').result binding } }
  bcmk.report("slim_test") { (1..2000).each{ __result = Slim::Template.new { slim_example }.render(context) } }
  bcmk.report("haml_test") { (1..2000).each { __result = Haml::Engine.new(haml_example).render(binding) } }
end

まず、必要なモジュールをインポートします。テンプレートエンジンに加え、ostructモジュールも読み込んでいます。OpenStructはメタプログラミング由来のデータ構造で、Hashに似ていますが、任意の属性とその値を自由に定義できるのが特徴です。

これは、値を格納するための完全なクラス構造を作る必要がないため便利です。インラインで定義できます。

今回の構造体は、タイトル、説明文、ランダムな数値のリストを持つNoteオブジェクトのようなもので、処理時間を増やす役割を果たします。

各テンプレートエンジンの動作詳細には踏み込みません。公式ドキュメントを参照してください。ただし、いずれの構文も非常に習得しやすいものです。

重要なポイントはコードの末尾にあります。ここではbmbmメソッドを使用しています。これは先ほどのmeasureよりも多機能なメソッドです。ベンチマーク結果は、ガベージコレクションやメモリリークなどの外部要因によって歪められることがあります。このメソッドは、テストを2回実行することでこの影響を最小限に抑えます。1回目はリハーサルとして実行環境を安定させ、2回目が本番の計測となります。詳しくは公式ドキュメントをご覧ください。

最後に、bmbmの内部の各行では、テンプレートの作成時に2000回のループを実行しています。焦点はテンプレートの割り当てとレンダリングに置かれています。

このテストファイルを実行すると、次の結果が得られます。

Rehearsal --------------------------------------------------------
erb_test               0.311534   0.002963   0.314497 (  0.314655)
slim_test              2.544711   0.004520   2.549231 (  2.550307)
haml_test              1.449813   0.003169   1.452982 (  1.454118)
----------------------------------------------- total: 4.316710sec

                           user     system      total        real
erb_test               0.298730   0.000679   0.299409 (  0.299631)
slim_test              2.550665   0.004148   2.554813 (  2.556023)
haml_test              1.432653   0.001984   1.434637 (  1.435417)

2つの結果ブロックが分かれており、リハーサルと実際の計測結果を区別できるようになっています。

シナリオを少し変えてみる

直前のテスト結果だけを見ると、ERBが最良の選択肢で、SLIMが最悪だと結論づけたくなるかもしれません。しかし、繰り返しますが、状況によります。

あのテストでは、ループのたびに新しいテンプレートエンジンのオブジェクトをインスタンス化していました。これは最適なフローではありません。

そこで、このインスタンス化を外側に移動するように、少しだけ変更してみましょう。以下のコードスニペットをご覧ください。

erb_engine = ERB.new(erb_example, 0, '-', '__result')
slim_engine = Slim::Template.new { slim_example }
haml_engine = Haml::Engine.new(haml_example)

Benchmark.bmbm(10) do |bcmk|
  bcmk.report("erb_test") { (1..2000).each { erb_engine.result binding } }
  bcmk.report("slim_test") { (1..2000).each{ __result = slim_engine.render(context) } }
  bcmk.report("haml_test") { (1..2000).each { __result = haml_engine.render(binding) } }
end

コードの内容は前とまったく同じです。もう一度テストを実行すると、次のような結果が得られます。

Rehearsal ----------------------------------------------
erb_test     0.127599   0.002407   0.130006 (  0.130137)
slim_test    0.046972   0.000841   0.047813 (  0.047858)
haml_test    0.208308   0.002239   0.210547 (  0.210769)
------------------------------------- total: 0.388366sec

                 user     system      total        real
erb_test     0.118002   0.000556   0.118558 (  0.118618)
slim_test    0.040129   0.000090   0.040219 (  0.040320)
haml_test    0.205331   0.001163   0.206494 (  0.206680)

今度はどれが最速で、どれが最遅かに注目してください。これはつまり、完璧なエンジンという「銀の弾丸」は存在しないことを示すための例です。

自分のコーディングスタイルにおいてどの機能がより良いパフォーマンスを発揮するのか、必ずテストと分析を行いましょう。さらに、Ruby Profilerなどの補助的なツールを活用すれば、コードがなぜ・どのようにそのように振る舞うのかをより深く理解できます。

実践的なシナリオでのロードテスト

次に、より現実に近いシナリオへ移りましょう。ノートの一覧を表示する実際のテンプレート(テンプレートエンジンごとに1つ、計3種類)をベンチマークします。

benchmarkモジュールはRailsコード内で動作するため、テンプレートエンジンの内部的なプロセスに関わる重要な計測項目のいくつかを失うことになります。

しかし、この種のベンチマークテストにより、リクエストの到着から始まり、ビジネスロジックの処理、レスポンスデータがビューに届くまでの一連の流れ全体として、各エンジンがどのようにパフォーマンスを発揮するかを確認できます。特に最後の部分には、解析やレンダリングといった多数のステップが含まれており、ロードテストならではの計測が可能で、benchmarkモジュールでは捉えられない領域です。

まず、各例に対応するRailsコントローラを作成しましょう。

rails g controller notes_erb index
rails g controller notes_haml index
rails g controller notes_slim index

このコマンドにより、お馴染みのRailsの各種ファイルが自動生成されます。

次に、生成されたnotes_erb_controller.rbファイルを開き、内容を以下のように変更します。

class NotesErbController < ApplicationController
  def index
    @notes = JSON.parse(Constants::NOTES, object_class: OpenStruct)
  end
end

ここでテンプレートにデータを供給しています。エンジンごとに1つのコントローラクラスがある点に注目してください。

基本的には、インライン定義された定数からJSONを取得しています。レスポンスは新しいOpenStructオブジェクトへパースされ、各エンジンへ渡されます。

NOTES定数は、constants.rbという新規ファイルに配置する必要があります。ファイルを作成し、以下の内容を追加してください。

class Constants
    NOTES = '[
        {
            "title": "Walk the dog",
            "description": "Bla bla",
            "tasks": [{
                "title": "Task #1"
            },
            {
                "title": "Task #2"
            },
            {
                "title": "Task #3"
            }]
        },
        ...
        {
            "title": "Walk the dog",
            "description": "Bla bla",
            "tasks": [{
                "title": "Task #1"
            },
            {
                "title": "Task #2"
            },
            {
                "title": "Task #3"
            }]
        }
    ]
    '
end

省略記号(...)の部分は、より多くのノート要素に置き換えてください。また、他のコントローラにも同じロジックを複製することをお忘れなく。

ERBビューの作成

さて、いよいよサンプルのビューを作成します。views/notes_erbフォルダに移動し、_note.html.erb_task.html.erbという2つのファイルを作成しましょう。これにより、ビュー・パーシャル・レイアウトを含むサンプルを構築できます。

こうすることで、Railsのテンプレートエンジンの機能を最大限に活用した例になります。views/notes_hamlviews/notes_slimについても同等のファイルを作成してください。

まずはindex.html.erbのコードから見ていきましょう。

<style>
h2 {
    text-align: center;
}

table, td, th {
  border: 1px solid #ddd;
  text-align: left;
}

table {
  border-collapse: collapse;
  width: 80%;
  margin: auto;
}

th, td {
  padding: 15px;
}
</style>
<h2>List of Notes</h2>
<table>
    <thead>
        <tr>
            <th>Title</th>
            <th>Description</th>
            <th>Tasks</th>
        </tr>
    </thead>
    <tbody>
        <%= render partial: 'notes_erb/note', collection: @notes %>
    </tbody>
</table>

特別な要素はほとんどありませんが、パーシャル(_note.html.erbファイル)を読み込み、collectionパラメータとして、コントローラで事前に作成した@notesを渡している点に注目してください。

ちなみに、ノートパーシャルの内容は以下の通りです。

<tr>
  <td>
    <span><%= note.title %></span>
  </td>
  <td>
    <span><%= note.description %></span>
  </td>
  <td>
    <ul>
      <%= render partial: 'notes_erb/task', collection: note.tasks %>
    </ul>
  </td>
</tr>

ここでもう1つのパーシャルが登場し、今回はtasks配列にアクセスしています。

_task.html.erbの内容は以下の通りです。

<li><%= task.title %></li>

HAMLのビュー

エンジン間で構文が非常によく似ていることに気づくでしょう。異なるのは記述の冗長さだけです。例えばSLIMは、3つの中で最もすっきりとした書き心地です。

3つのファイルのコードを見てみましょう。

# Content of index.html.haml
:css
  h2 {
      text-align: center;
  }

  table, td, th {
    border: 1px solid #ddd;
    text-align: left;
  }

  table {
    border-collapse: collapse;
    width: 80%;
    margin: auto;
  }

  th, td {
    padding: 15px;
  }

%h2 List of Notes
%table
  %thead
    %tr
      %th Title
      %th Description
      %th Tasks

  %tbody
    = render partial: 'notes_haml/note', collection: @notes

# Content of _note.html.haml
%tr
  %td
    %span= note.title


  %td
    %span= note.description


  %td
    %ul
      = render partial: 'notes_haml/task', collection: note.tasks

# Content of _task.html.haml
%li= task.title

とてもよく似ていますよね?

SLIMのビュー

最後はSLIMのビューです。ここが他の2つとの最大の違いです。構造全体がより明快になります。

# index.html.slim
css:
  h2 {
      text-align: center;
  }

  table, td, th {
    border: 1px solid #ddd;
    text-align: left;
  }

  table {
    border-collapse: collapse;
    width: 80%;
    margin: auto;
  }

  th, td {
    padding: 15px;
  }

h2 List of Notes
table
  thead
    tr
      th Title
      th Description
      th Tasks

  tbody
    = render partial: 'notes_haml/note', collection: @notes

# _note.html.slim
tr
  td
    span= note.title


  td
    span= note.description


  td
    ul
      = render partial: 'notes_haml/task', collection: note.tasks

# _task.html.slim
li= task.title

また、レイアウトもそれぞれのエンジンの構文に翻訳する必要があります。views/layoutsフォルダ配下に、application.html.hamlapplication.html.slimという2つの新規ファイルを作成してください。

この作業は宿題として皆さんにお任せします。もし難しい場合は、記事の最後に紹介するGitHubプロジェクトのリンクから私の実装例を参照できます。

テストの実行

ついにサンプルをテストする段階になりました。まず、rails sコマンドでアプリケーションを起動します。https://localhost:3000/ のアドレスで起動するはずです。

ビューの見た目は以下のようになります。

最速のRubyテンプレートエンジンはどれ?ERB vs HAML vs Slim 徹底パフォーマンス比較

各テンプレートエンジンのサンプルには、config/routes.rbファイルで自動生成されたそれぞれのURLからアクセスできます。

これらのサンプルをベンチマークテストするために、heyベンチマークツールを使用します。非常にシンプルでありながら、ベンチマーク分析に役立つ有用な情報を提供してくれます。コマンドは以下の通りです。

$ hey https://localhost:3000/notes_erb/index

Summary:
  Total:        9.3978 secs
  Slowest:      9.1718 secs
  Fastest:      0.0361 secs
  Average:      1.2714 secs
  Requests/sec: 21.2816

$ hey https://localhost:3000/notes_haml/index
Summary:
  Total:        10.8661 secs
  Slowest:      10.2354 secs
  Fastest:      0.1871 secs
  Average:      1.4735 secs
  Requests/sec: 18.4058

$ hey https://localhost:3000/notes_slim/index

Summary:
  Total:        11.3384 secs
  Slowest:      10.7570 secs
  Fastest:      0.0437 secs
  Average:      1.5406 secs
  Requests/sec: 17.6392

ご覧の通り、すべてのエンジンの実行時間はかなり接近しています。デフォルトのリクエスト数は200ですが、-nオプションでこの値を変更できます。

同じテストを1200リクエストで実行した結果も見てみましょう。

$ hey -n 1200 https://localhost:3000/notes_erb/index

Summary:
  Total:        52.2586 secs
  Slowest:      19.2837 secs
  Fastest:      0.0389 secs
  Average:      0.6960 secs
  Requests/sec: 22.9627

$ hey -n 1200 https://localhost:3000/notes_haml/index
Summary:
  Total:        61.7637 secs
  Slowest:      18.5290 secs
  Fastest:      0.0442 secs
  Average:      0.8557 secs
  Requests/sec: 19.4289

$ hey -n 1200 https://localhost:3000/notes_slim/index

Summary:
  Total:        63.1625 secs
  Slowest:      19.9744 secs
  Fastest:      0.0874 secs
  Average:      0.7959 secs
  Requests/sec: 18.9986

同時リクエスト数を増やすと、総処理時間と平均処理時間の差が広がることが分かります。もちろん、数千件の並列リクエストというこのシナリオは非常に特殊で、日常的ではありません。しかしそここそがロードテストの醍醐味です。エンドポイントを限界まで追い込むことで初めて見えるものがあるのです。

heyツールは、レスポンスタイムのヒストグラムなど、その他の情報も出力してくれます。

最速のRubyテンプレートエンジンはどれ?ERB vs HAML vs Slim 徹底パフォーマンス比較

ヒストグラムには、各リクエストの完了までのおおよその平均所要時間が表示されます。今回の例では、大多数のリクエスト(1048件)が約1.893秒以内に完了していたことが分かります。だからこそ、ストレステストを同時に実施することが望ましいのです。

さらに、レイテンシ分布、DNS接続・名前解決の詳細、リクエスト書き込み・待機・読み取り時間、エラーなどについても情報が得られます。

カスタムオプションや結果の詳細については、公式ドキュメントを確認してください。

まとめ

本記事のサンプルのソースコードは、こちらから入手できます。

この種のテストは、プロジェクトのニーズにどのテンプレートエンジンが適しているかを判断するのに非常に役立ちます。見てきたように、実装の不十分なコードには注意が必要です。小さなコード断片ひとつで、全体的な実行パフォーマンスが劇的に変化することがあるからです。

もう1つ興味深い点として、RailsのデフォルトエンジンであるERBも、その役割を十分に果たしていることが挙げられます。他のエンジンの方が一部のテストで高速だったとしても、ERBは常に僅差で食い下がり、その価値を証明しています。

最後に、宿題として、キャッシュ、プロキシ、データベースやその他のストレージ機構の利用、キューなど非同期処理に関わるツールといった、テストにおいて考慮すべき重要な要素も検討してみることをおすすめします。これらの要素は常に、ビューの振る舞いやレンダリングにかかる処理時間に大きな影響を与えます。それでは、良いテストライフを!

  1. お住まいの地域で最速のVPNはどれ?無料ツール「VPNify」が教えてくれる

    VPNは違法行為のためだけのものではない 「VPNはファイル共有などの違法行為をする人だけが必要なもの」と思っていませんか?実はそうではありません。自分自身を守るため、あるいは不安定なネットワークのパフォーマンスを改善するために、VPNを使うべき場面は数多くあります。 例えば、海外に一時的に滞在している間でも、VPNを使えばアメリカ版Netflixにアクセスできます。近年NetflixがVPNへの取り締まりを強化し、多くのユーザーが影響を受けましたが、それ以外にもVPNの活用方法はたくさんあります。検索履歴のプライバシー保護、国際VoIP通話、さらには公衆Wi-Fi利用時のセキュリティ確保など

  2. Ruby開発に最適なIDE・エディタはどれ?人気エディタを徹底比較

    開発者としての時間の大半はエディタ上で過ごすことになります。だからこそ、自分が快適に使えて、生産性を高めてくれるエディタを選ぶことがとても重要です。 この記事では、自分に合ったRuby IDE/エディタの選び方を解説します。 この決断で悩みすぎる必要はありません。まずは1つ選んで数週間使ってみて、使い心地を確かめましょう。後から乗り換えることもいつでもできます。 エディタ選びでチェックしたいポイント Ruby関連のプラグインが充実しているか オープンソースかどうか 生産性を高める標準機能があるか(コードスニペット、自動補完、邪魔にならないシンプルなデザインなど) 以下で紹介するエディタは、