Speeding up views rendering in Rails 4

Diario del capitán, fecha estelar d69.y40/AB

This week, I have had some fun debugging a performance problem related to views rendering outside of a controller in a Rails 4 application.

Lights on highway - Photo by Marc Sendra Martorell

It is well known that rendering views and partials in Rails is somewhat slow compared to other frameworks. In one particular part of the Rails migration for a large corporate client of ours, I had to migrate an endpoint rendering ~ 200 views. Using the standard render approach, the request took ~ 23s. Now, it's down to ~ 300ms.

Want to know how I did it? Keep on reading! 👇

This is Rails 4, so the standard approach to render a view outside of a controller is this:

    renderer = ActionView::Base.new(ActionView::Base.paths, {})

I used RubyProf to analyse this piece of code and it was taking ~ 150ms for each view just to find the file to read from the filesystem! Under the hood, ActionView builds a query to use with the _Dir_ method in Ruby in order to locate the file. The problem is that the query contains all the possible extensions and handlers, something like this:


In order to make this query simpler, a custom LookupContext can be given, like this:

    lookup_context = ActionView::LookupContext.new(
      locale: [],
      formats: [:html],
      variants: [],
      handlers: [:erb]
    renderer = ActionView::Base.new(lookup_context, {})

The arguments given to the object are self-explanatory. Basically we are telling ActionView to only search for .html.erb files, because in this particular scenario all the views are created this way. This already provided a massive improvement but we could still do better.

Under the hood, when we call render, ActionView initializes an ActionView::Template object and calls render on it. In order to build this object, it needs to find the file in the filesystem and do other operations. In this particular scenario, we already know the exact path of the view to read, so there is no need for Rails to do the effort. So, I ended up creating Template objects manually:

    template_contents = File.binread(path_to_view)
    handler = ActionView::Template.handler_for_extension(:erb)
    ActionView::Template.new(template_contents, file_name, handler,
                             format: :html,
                             locals: ['var1', 'var2'])

    renderer = ActionView::Base.new(lookup_context, {})
    renderer.render(template: template, locals: { var1: 'foo', var2: 'bar' })

This piece of code basically skips almost all the rendering code and just evaluates the file with ERB to produce the output. The locals parameter given to the instance needs to match the keys of the locals hash passed to the render call. The rest of parameters are self-explanatory.

Pretty cool stuff, isn't it? 🚀

Oriol Collell Martín

Oriol Collell Martín

Chief MartianTapas Officer. Antes de MarsBased, era co-fundador y CTO en Dineyo, donde aprendió a emprender. Hooligan de Startup Grind y de las Muns. Toda empresa tiene un troll, nosotros tenemos a Oriol.

comments powered by Disqus

Estás a un paso de conocer a tu mejor socio.