Which is fastest? ERB vs. HAML vs. Slim
Tags: ruby
Published: 2021-04-27 17:41:30

*This article was originally written by [Diogo Souza][1] on the [Honeybadger Developer Blog][2].*

In this article, we’ll test and analyze the performance of three most popular Ruby templating engines: ERB (the default one), [HAML](http://haml.info/), and [SLIM](http://slim-lang.com/).

[Benchmarking](https://en.wikipedia.org/wiki/Benchmarking) is the practice of comparing business processes and performance metrics to industry bests and best practices from other companies. Meanwhile, [load testing](https://en.wikipedia.org/wiki/Load_testing) is the process of putting demand on a system and measuring its response.

Our goal is to explore a little bit of the Ruby [Benchmark module](https://ruby-doc.org/stdlib-2.5.3/libdoc/benchmark/rdoc/Benchmark.html), which provides methods to measure and report the time used to execute Ruby code. We’ll create some inline templates, run them against the tests, and extract metrics over the three different engines.

After that, we'll dive into load testing by creating some real-world template views, put them live on the server, and then perform some load tests via the [hey](https://github.com/rakyll/hey) tool. It provides us with some great features to send loads to a specific web application and get thorough data related to how each endpoint and, hence, each template engine is performing.

## Setup

The first thing to do, of course, is to make sure you already have [Ruby](https://www.ruby-lang.org/en/) installed. We’re using the most recent version, 2.7.0, as of the writing of this article. Make sure to also [install the Rails gem](https://guides.rubyonrails.org/v5.0/getting_started.html).

Ruby and the Rails gem are pretty much what we need to get started. For this tutorial, Visual Studio Code is the IDE for coding, but feel free to pick another if you prefer.

Now, choose a folder for our Rails project and run the following command:

```shell
rails new haml-slim-erb
```

This will download all the required dependencies and create our scaffolded Rails project. Go ahead and explore it.

Before proceeding to the code, we need to add the SLIM and HAML dependencies to our _Gemfile_:

```ruby
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
```

There’s also a bug that comes by default with the project that’s related to the SQLite version. Locate it in the _Gemfile_ and change it to the following:

```ruby
gem 'sqlite3', '~> 1.3.0'
```

Now, run the `bundle install` command to download the dependencies.

## Exploring the Benchmark Module

For this part of the article, we’ll be working directly with the _test_ folder. Open it, and you’ll see some empty folders. Let’s create a new one called _benchmark_ and three other files: _example_1_test.rb_, _example_2_test.rb_, and _example_3_test.rb._.

Ruby needs that t end with _test_ to be considered a test file.

Next, add the following content to the first file:

```ruby
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
}
```

Note that the first line imports the required Benchmark module. Then, we generate a random array of numbers from 0 to 50 with 10.000 of size. These big numbers are just to take some time in processing.

The method [`measure`](https://ruby-doc.org/stdlib-2.5.3/libdoc/benchmark/rdoc/Benchmark.html#method-c-measure) is very useful because it can be placed anywhere in your Ruby code to measure how much time it’s taking to process. This time is returned and printed by the `puts`.

Inside, we’re looping for 20k times the execution of the same random-generated array to multiply one value of each.

To run this test file specifically, issue the following command:

```shell
rake test TEST=test/benchmark/example_1_test.rb
```

The result may look like this:

```shell
0.702647   0.012353   0.715000 (  0.721910)
```

This report prints, respectively, the user CPU time, the system CPU time, the sum of the user and system CPU times, and the elapsed real-time. The unit of time is seconds.

We’ll make use of the other Benchmark methods further in practice.

## Inline Templates Testing

Now that you understand a bit more about how Ruby’s Benchmark module works, we’ll dive into the execution of some tests over the three templates.

For this, we’ll create a single template, translate it into the three engine syntax, and finally, run it under the Benchmark method.

Add the following to the second test file:

```ruby
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
```

First, import the required modules. Other than the template engines, we’re also importing the [`ostruct`](https://ruby-doc.org/stdlib-2.5.1/libdoc/ostruct/rdoc/OpenStruct.html) module. An `OpenStruct` is a data structure from metaprogramming, similar to a `Hash`, that allows the definition of arbitrary attributes with their accompanying values.

It is useful because we don’t need to create a whole class structure to store the values. We can define it inline.

Our struct is basically a `Note` object with a title, description, and a list of random numbers, which increase processing time.

We won’t focus on how each template engine works; you can refer to their official docs for this. However, their syntax is pretty easy to assimilate.

The magic is placed at the end of the code. Now, we’re using the [`bmbm`](https://ruby-doc.org/stdlib-2.5.3/libdoc/benchmark/rdoc/Benchmark.html#method-c-bmbm) method, which does a bit more than the first one. Sometimes, benchmark results may be skewed due to external factors, such as garbage collection, memory leaks, etc. This method attempts to minimize this effect by running the tests twice: the first time as a rehearsal to get the runtime environment stable and the second time for real. You can read more on this [here](https://ruby-doc.org/stdlib-2.5.3/libdoc/benchmark/rdoc/Benchmark.html#method-c-bmbm).

Finally, each one of the `bmbm`’s inner code lines executes a loop 2000 times during the creation of each template. The focus is on assigning the template and rendering it.

After executing this test file, here’s the result:

```shell
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)
```

The two result blocks are separated for you to assimilate what’s real from the rehearsal.

## Changing the Scenario a Bit

For the last test results, you could assume that ERB is the best option, while SLIM is the worst. Again, it depends on the situation.

In that test, every time we loop, we have to instantiate a _new_ template engine object. This is not an optimal flow.

Let’s change it slightly and move this instantiation to the outside, as shown in the following code snippet:

```ruby
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
```

The code does exactly the same as before. Now, run the tests again, and you’ll see something like this as a result:

```shell
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)
```

Notice which one is the best and the worst now. This is just to show that there is no silver bullet in terms of a perfect engine.

You must test and analyze which features perform better for your style of coding. Plus, there are other auxiliary tools, such as [Ruby Profiler](https://ruby-prof.github.io/), that you can use to understand better how and why the code behaves this way.

## Load Testing a Real-World Scenario

Let’s move on to something closer to our reality. We’re going to benchmark a real template that lists some notes (three, one for each template engine).

Since the `benchmark` module takes place in the Rails code, we lose some important measures related to the template engine internal processes.

This type of benchmark test allows us to see how each engine performs as a whole, from the very beginning of the process via the arrival of a request, to the business logic processing until the response data gets to the views. This last one, specifically, will have a bunch of steps, such as the parsing and rendering processes that the load test can measure, and the `benchmark` cannot.

First, let’s create the Rails controllers for each example:

```shell
rails g controller notes_erb index
rails g controller notes_haml index
rails g controller notes_slim index
```

This command will auto-generate a bunch of common Rails files you may be used to.

Next, let’s open the _notes_erb_controller.rb_ created file and change its content to:

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

Here’s where we feed the templates with data. Note that we have one controller class for each of the engines.

Basically, we’re grabbing some JSON from an inline constant. The response is going to be parsed into a new `OpenStruct` object and returned to the engines.

The `NOTES` constant must be placed into a new file called _constants.rb_. Go ahead and create it, and then add the following content:

```ruby
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
```

Please, make sure to change the ellipsis for more note elements. Plus, don’t forget to replicate the logic in each one of the other controllers.

## Creating the ERB Views

Now, it’s time to create the views of our example. To do so, go to the `views/notes_erb` folder and create two other files: `note.html.erb` and `task.html.erb`. They’ll help us build an example that contains views, partials, and layouts.

This way, we guarantee our example explored the Rails engines the most. Make sure to create files equivalent to both `views/notes_haml` and `views/notes_slim`.

Let’s start with the `index.html.erb` code:

```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>
```

There's nothing too special here. Note that we’re importing a partial, the `_note.html.erb` file, and passing a `collection` parameter: our `@notes` previously created in the controller.

Here’s the note’s content, by the way:

```erb
<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>
```

Here's another partial, accessing the `tasks` array this time.

The content for the `_task.html.erb` is the following:

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

## HAML’s Views

You will notice that the syntax from one engine to another is very similar. What changes is just the verbosity of each one. SLIM, for example, is the cleanest one among them.

Take a look at the code for the three files:

```haml
# 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
```

Very similar, isn't it?

## SLIM’s Views

Finally, we have SLIM's views. Here’s the greatest difference from the other two ones. The whole structure becomes clearer:

```slim
# 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
```

You’ll also have to translate the layouts to each respective engine syntax. Two new files have to be created under the _views/layouts_ folder: _application.html.haml_ and _application.html.slim_.

I’ll leave that job to you as homework. However, if you find it difficult, you can consult my version in the GitHub project link available at the end of the article.

## Running the Tests

Finally, we get to test the example. First, start up the application by running the `rails s` command. It’ll start at the [http://localhost:3000/](http://localhost:3000/) address.

Here’s what the view will look like:

![List of Notes](https://www.honeybadger.io/blog/images/blog/posts/ruby-template-performance-erb-haml-slim/Figure01.png)

Each template engine example will be available at its respective URL auto-generated in the _config/routes.rb_ file.

To benchmark test these examples, we’ll make use of the [hey](https://github.com/rakyll/hey) benchmark tool. It is very simple and provides some useful information for benchmark analysis. These are the commands:

```shell
$ hey http://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 http://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 http://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
```

As you can see, all the engines are very close in terms of execution time. The default number of requests to run is 200, but you can change this value via the `-n` option.

Let's take a look at the same tests performed with 1200 requests:

```shell
$ hey -n 1200 http://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 http://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 http://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
```

When you increase the number of concurrent requests, you'll see differences in total and average processing times increase. Obviously, this scenario, with thousands of parallel requests, is very specific and not very common. However, load testing is all about that, taking the endpoints to the limits.

The hey tool also prints other information, such as a response time histogram:

![Histogram](https://www.honeybadger.io/blog/images/blog/posts/ruby-template-performance-erb-haml-slim/Figure02.png)

It shows the average time each request took to complete in approximate terms. In our example, it’s clear that most of the requests (1048) were completed in 1.893 seconds. This is why it's desirable to perform stress tests at the same time.

There’s also more info about latency distribution, details over the DNS dialup and lookup, request writing, waiting and reading times, errors, etc.

Check the [docs](https://github.com/rakyll/hey) for more custom options/results.

## Summary

You can find the source code for this example [here](https://github.com/diogosouza/haml-slim-erb).

This kind of test is great for helping you define which template engine may be more suitable for your project needs. As we’ve seen, be careful about poorly implemented code since some small code snippets can abruptly change the overall performance of your execution.

Another interesting point is that ERB, as the default Rails engine, is great at its job too. Besides the other engines being faster in some of our tests, ERB is always close enough to prove its value.

Finally, I’d recommend that you consider some other important factors in the tests as a homework task, including caching, proxy, the usage of databases, and other storing mechanisms, as well as queues and any such tool of an async nature. These items always play an important role in the way your views behave or the processing time it takes to render them. Good luck!
[1]:https://www.honeybadger.io/blog/ruby-template-performance-erb-haml-slim/#authorDetails
[2]:https://www.honeybadger.io/blog/ruby-template-performance-erb-haml-slim/