Rails Partial Performance

October 6, 2021

Partials can help keep your Rails views organized. They can split up a large template into manageable pieces and make it easy to share code.

But they can be surprisingly inefficient. This becomes most noticeable in loops.

Let's say I have a states#index page where I need to show a list of info about all 50 U.S. states. So I start to write a view like:

<% @states.each do |state| %>
  <div><%= state.name %></div>
<% end %>

For this example I'm just printing the state name, but you could imagine that the content for a given state would be more involved than this.

What if I need to display this content on the states#show page in exactly the same way? I could extract this into a new partial:

<div><%= state.name %></div>

And change the index page to use that:

<% @states.each do |state| %>
  <%= render partial: 'states/summary', locals: { state: state } %>
<% end %>

When I do this, the time it takes to render my index page goes from ~7ms to ~21ms. 🤔 The overhead of using a partial made the page load 3 times slower. In this case, it's only a few milliseconds different, but this can start to add up depending on how frequently partials are used and how much info you're required to display on the page.

What are some alternatives?

We could use the collection option for the partial. This will let Rails render the collection in a more efficient way:

<%= render partial: 'states/summary', collection: @states %>

Or we could skip the partial altogether and define a helper method that uses Ruby to build HTML:

def display_state(state)
  content_tag :div, state.name
end

And then use that on the index page:

<% @states.each do |state| %>
  <%= display_state(state) %>
<% end %>

Here are some benchmarks that give an idea of how these options compare. I created a brand new Rails 6.1 app and added these example view templates to display the numbers from 1 to 50:

<%# app/views/example/_value.html.erb %>
<div><%= value %></div>

<%# app/views/examples/single_view_template.html.erb %>
<% values = (1..50).to_a %>
<% values.each do |value| %>
  <div><%= value %></div>
<% end %>

<%# app/views/examples/partial.html.erb %>
<% values = (1..50).to_a %>
<% values.each do |value| %>
  <%= render partial: 'examples/value', locals: { value: value } %>
<% end %>

<%# app/views/examples/partial_collection.html.erb %>
<% values = (1..50).to_a %>
<%= render partial: 'examples/value', collection: values %>

<%# app/views/examples/helper_method.html.erb %>
<% values = (1..50).to_a %>
<% values.each do |value| %>
  <%= show_value(value) %>
<% end %>

This is the show_value helper method:

def show_value(value)
  content_tag :div, value
end

Then I ran benchmarks with:

require 'benchmark'

Rails.logger.level = :fatal  # Turn off logging of view info

def test_render(template)
  100.times { ApplicationController.render(template: template) }
end

Benchmark.bmbm do |b|
  b.report('Single View Template') { test_render('examples/single_view_template') }
  b.report('Partial') { test_render('examples/partial') }
  b.report('Partial Collection') { test_render('examples/partial_collection') }
  b.report('Helper Method') { test_render('examples/helper_method') }
end

These are the results:

Rehearsal --------------------------------------------------------
Single View Template   0.347008   0.138744   0.485752 (  0.593323)
Partial                0.485201   0.112485   0.597686 (  0.635593)
Partial Collection     0.283788   0.099981   0.383769 (  0.411720)
Helper Method          0.279053   0.100516   0.379569 (  0.408297)
----------------------------------------------- total: 1.846776sec

                           user     system      total        real
Single View Template   0.259254   0.100171   0.359425 (  0.389293)
Partial                0.447392   0.101954   0.549346 (  0.579761)
Partial Collection     0.299114   0.104745   0.403859 (  0.449048)
Helper Method          0.274448   0.101476   0.375924 (  0.404484)

And here were the results when I updated the views to print 100 values instead of 50:

Rehearsal --------------------------------------------------------
Single View Template   0.292736   0.110953   0.403689 (  0.453713)
Partial                0.650573   0.109569   0.760142 (  0.793807)
Partial Collection     0.306908   0.100607   0.407515 (  0.434600)
Helper Method          0.290522   0.099390   0.389912 (  0.417302)
----------------------------------------------- total: 1.961258sec

                           user     system      total        real
Single View Template   0.260891   0.099110   0.360001 (  0.385955)
Partial                0.635798   0.105802   0.741600 (  0.773008)
Partial Collection     0.302778   0.100124   0.402902 (  0.431215)
Helper Method          0.286351   0.099417   0.385768 (  0.413537)

Keeping everything in a single view template is the fastest. Refactoring code with helper methods is almost as fast. Refactoring with partials that can take advantage of the collection option is also fast. But rendering a large number of independent partials can slow things down in a hurry.

Organizing view code is a balance between what's easy to maintain and what performs at an acceptable level.

Pros Cons
Large View Template Fast, Easy to read Not DRY, Frequent merge conflicts
Partials Easy to read, DRY Noticeably slows page loads if overused
Helper Methods Fast, DRY Difficult to read when building complex HTML, Easy to forget to escape values

Sometimes a blend of these different options is the best approach.


References