Benchmark de template engines en Ruby on Rails

20 de octubre del 2020 鈥 1573 palabras 鈥 8 min

Dicen por ah铆, en estos lares programadores de internet, que si tus esfuerzos de optimizaci贸n se concentran en las vistas, algo estar谩s haciendo mal.

Personalmente difiero, ya que nunca sabemos qu茅 tama帽o tendr谩 la aplicaci贸n, cu谩nta deuda t茅cnica existe y cu谩l es la m茅trica particular que nos est谩 comiendo milisegundos en un proyecto.

En este art铆culo exploro qu茅 template engine tiene mejor performance, incluyendo ERB, HAML y Slim.

Puedes ir directamente a los Resultados del benchmark en caso de que quieras ir directamente a los resultados.

驴A qu茅 le haremos benchmark?

El setup es una aplicaci贸n fresca de Rails utilizando Ruby 2.7.1p83 con Rails 6.0.3.4 generada con el siguiente comando:

$ rails new app_name

Este benchmark est谩 enfocado al tiempo de renderizado de las vistas de los siguientes template engines:

Por lo que utilizar谩n las siguientes gemas, esto para replicar lo m谩s cercano a un setup com煤n.

gem 'slim-rails', '~> 3.2.0'
gem 'haml-rails', '~> 2.0.1'

ERB viene por default en Ruby, por lo que no es necesario agregarlo.

Por otro lado, dentro de la aplicaci贸n tenemos 3 modelos: usuarios, skills y skills de usuario, que conecta al usuario con varios skills.

Diagrama de modelos en Rails Diagrama de modelos en Rails

Para rellenar esta data utilic茅 la gema ffaker.

gem 'ffaker', '~> 2.17.0'

Y escrib铆 un archivo de seeds para llenar mi base de datos.

require 'ffaker'

skills = []
30.times do
  skills << { name: FFaker::Skill.tech_skill }
end
Skill.create!(skills)

users = []
1000.times do
  users << {
    first_name: FFaker::Name.first_name,
    last_name: FFaker::Name.last_name,
    bio: FFaker::DizzleIpsum.paragraph,
    skill_ids: Skill.order('RANDOM()').limit(5).pluck(:id)
  }
end
User.create!(users)

En donde los 1000 usuarios que genero tiene 5 skills random asignados de los 30 existentes.

Despu茅s tenemos una ruta para cada engine, as铆 como un layout y un set de vistas, que renderean todos los usuarios y sus skills son 3 parciales de profundidad. Por ejemplo, para ERB:

<%= render partial: 'shared/erb/user', collection: @users %>

<!-- shared/erb/user -->
<h3><%= user.first_name %> <%= user.last_name %></h3>
<p><%= user.bio %></p>
<h4>Skills</h4>
<ul>
  <%= render partial: 'shared/erb/skill', collection: user.skills %>
</ul>
<hr />

<!-- shared/erb/skill -->
<li><%= skill.name %></li>

Limitamos el rendering a 50 elementos desde el controlador para evitar tanto tiempo de render.

class PagesController < ApplicationController
  layout :erb, only: :erb
  layout :haml, only: :haml
  layout :slim, only: :slim

  before_action :set_data

  # GET /erb
  def erb; end

  # GET /haml
  def haml; end

  # GET /slim
  def slim; end

  private

  def set_data
    @users = User.includes(:skills).limit(50)
  end
end

D谩ndonos como resultado la siguiente anidaci贸n de parciales en cada vista de cada tipo de engine.

Diagrama de vistas en Rails Diagrama de vistas en Rails

驴Con qu茅 haremos benchmark?

Lo haremos con ApacheBench, o ab para los amigos.

El contexto del benchmark es el siguiente:

Correremos ab en cada una de las rutas que creamos, en un s贸lo thread, con un m谩ximo de 500 requests.

$ ab -n 500 -c 1 http://localhost:3000/erb
$ ab -n 500 -c 1 http://localhost:3000/haml
$ ab -n 500 -c 1 http://localhost:3000/slim

Pero ojo, estos benchmark estar谩n evaluando el request completo, no el rendereado de vistas que es lo que nos interesa.

Afortunadamente en Rails podemos suscribirnos a eventos a trav茅s de ActiveSupport::Notification para obtener la duraci贸n de cada uno de los parciales y de las vistas. Esto nos ayudar谩 a tener m茅tricas a煤n mas realistas que las anteriores.

Agregu茅 un initializer para esto, que simplemente escribir谩 en un CSV con informaci贸n relevante. Quiz谩s no sea la manera m谩s 贸ptima de hacerlo, pero en base a las pruebas que hice, la escritura del CSV no afecta en la m茅trica de renderizaci贸n.

ActiveSupport::Notifications.subscribe /^render_.+.action_view$/ do |event|
  CSV.open('render_data.csv', 'a') do |row|
    view_engine = event.payload[:identifier].split('.').last
    row << [
      view_engine,
      event.payload[:identifier],
      event.time,
      event.end,
      event.duration
    ]
  end
end

Esto, combinado con ab, nos dar谩 por cada request la siguiente data:

Aqu铆 podr铆amos hacer algo muy chic y graficar autom谩ticamente, pero lo que termin茅 haciendo fue importar el CSV a un Google Sheets y hacerlo a partir de ah铆.

Resultados del benchmark

La siguiente informaci贸n fue recabada dado el setup mencionado anteriormente.

El resultado de cada ejecuci贸n que se hizo con la herramienta ab.

Document Path:          /erb
Document Length:        28932 bytes

Concurrency Level:      1
Time taken for tests:   14.898 seconds
Complete requests:      500
Failed requests:        0
Total transferred:      14687000 bytes
HTML transferred:       14466000 bytes
Requests per second:    33.56 [#/sec] (mean)
Time per request:       29.796 [ms] (mean)
Time per request:       29.796 [ms] (mean, across all concurrent requests)
Transfer rate:          962.72 [Kbytes/sec] received
Document Path:          /haml
Document Length:        28832 bytes

Concurrency Level:      1
Time taken for tests:   15.821 seconds
Complete requests:      500
Failed requests:        0
Total transferred:      14637000 bytes
HTML transferred:       14416000 bytes
Requests per second:    31.60 [#/sec] (mean)
Time per request:       31.642 [ms] (mean)
Time per request:       31.642 [ms] (mean, across all concurrent requests)
Transfer rate:          903.48 [Kbytes/sec] received
Document Path:          /slim
Document Length:        28180 bytes

Concurrency Level:      1
Time taken for tests:   14.628 seconds
Complete requests:      500
Failed requests:        0
Total transferred:      14311000 bytes
HTML transferred:       14090000 bytes
Requests per second:    34.18 [#/sec] (mean)
Time per request:       29.256 [ms] (mean)
Time per request:       29.256 [ms] (mean, across all concurrent requests)
Transfer rate:          955.40 [Kbytes/sec] received

A partir de estos requests, se gener贸 un set de resultados que en total sumaron 78,000 puntos. De estos se condens贸 el tiempo de render de los parciales y se concentr贸 la data en el punto de render de la vista completa.

Tipo Promedio Media
ERB 30.44760273 27.824637
HAML 32.38679421 29.7028665
Slim 30.39951217 27.8748165

Finalmente, la misma informaci贸n de la tabla anterior pero en un gr谩fico.

Gr谩fico de duraci贸n de render promedio y media

Conclusiones

Me ha impresionado el performance que tiene Slim. Por mi experiencia sab铆a que HAML no era tan r谩pido como ERB, pero no hab铆a pensando que hasta el d铆a de hoy, Slim tuviera un performance muy parecido, y hasta a veces superior a mism铆simo ERB.

Sin duda hay mucho que explorar a煤n, pero si tuviera que elegir un template engine para mi pr贸ximo proyecto, lo har铆a con Slim. Adem谩s de ser muy natural - o a la CSS, se comporta bastante bien.

Y como pie de nota, este benchmark lo hice a partir de una necesidad de performance que surgi贸 en Domestika.org, el cual por el volumen que tenemos, cada milisegundo es oro.

Enlaces

Este art铆culo no me lo saqu茅 del zapato, aqu铆 les dejo un par de enlaces que me ayudaron a encaminar este test. Muchas gracias a sus autores.

Finalmente, puedes ver el repositorio del proyecto generado para este art铆culo aqu铆: kinduff/rails_template_engine_benchmark.