Rails view rendering performance: when render gets expensive
Rails makes rendering feel almost weightless. You call render, Rails finds the right partial, binds the locals, gives you a nice little slice of HTML, and lets you move on with your day. That is exactly why partials are so useful. They compress a lot of ceremony into one friendly line.
We have multiple Rails view rendering strategies: inline ERB, rendering a partial inside a loop, collection rendering, implicit rendering, and a helper-based experiment using content_tag. The numbers are not subtle. In a Rails 8 benchmark, inline ERB was fastest. Rendering a partial inside a loop was much slower. Collection rendering and implicit rendering landed in the middle. The helper experiment was the fun twist: it looked like a clever way to avoid render, but it performed about as badly as the slow partial loop.
That result is a good reminder for Rails teams. The view layer can become part of your performance story, especially when a page renders many repeated objects. It is rarely the first thing to blame, and it is almost never a reason to turn every page into one giant ERB file. Still, the cost exists. Once you know where it comes from, you can make better trade-offs.
What Rails does when you render a partial
When you write a small partial, it feels like Rails just pastes a file into the current template. That mental model is convenient, but incomplete.
Imagine a dashboard that renders a list of invoices:
<h1>Invoices</h1>
<% @invoices.each do |invoice| %>
<%= render "invoices/row", invoice: invoice %>
<% end %>
Every iteration asks Rails to render that partial. Rails has optimizations, including compiled templates and caching, so it is not doing the dumbest possible thing. But each render call still has work around the template execution itself. Rails needs to resolve the template, prepare rendering machinery, bind locals, and execute the template method.
That is fine for a handful of rows. It becomes more visible when the loop grows. If you have 1,000 invoices, you did not write one partial render. You wrote 1,000 partial renders.
The dangerous part is not that partials are bad. They are not. The dangerous part is assuming that because the code is small, the runtime shape is small too.
Collection rendering is the Rails-shaped optimization
Rails already gives you a better shape for repeated partials:
<h1>Invoices</h1>
<%= render partial: "invoices/row", collection: @invoices, as: :invoice %>
This keeps the maintainability benefit of a partial while letting Rails handle the collection as a collection. In that benchmark, collection rendering was much faster than calling render manually inside the loop. The reason is simple enough: some setup work can happen once for the collection instead of once per item.
I like this version because it is explicit. You can see the partial path. You can see the local name. You can move the partial without needing to ask the model what it thinks its partial path should be.
Implicit rendering is even shorter:
<h1>Invoices</h1>
<%= render @invoices %>
That works because Rails asks each object for its partial path. Most of the time, Active Model gives you the conventional answer. For an Invoice, Rails can infer something like "invoices/invoice".
There is nothing wrong with that style when the convention is obvious. In a large codebase, though, I usually prefer the explicit collection form for important screens. It keeps the decision in the view, where the rendering decision is being made.
Inline ERB wins the benchmark, but not every code review
Inline ERB was the fastest strategy in the benchmark because it avoids the render path entirely. There is no partial lookup, no locals binding for a separate template, and no separate rendering call per item.
A fully inline version might look like this:
<h1>Invoices</h1>
<% @invoices.each do |invoice| %>
<article class="invoice-row">
<strong><%= invoice.number %></strong>
<span><%= number_to_currency(invoice.total_cents / 100.0) %></span>
<time datetime="<%= invoice.issued_on.iso8601 %>">
<%= invoice.issued_on.to_fs(:long) %>
</time>
</article>
<% end %>
That is hard to beat mechanically. It is also easy to abuse.
If the markup is tiny, unique to one page, and showing up hot in a profiler, inlining can be the right move. If the markup is reused in three places, has branching states, or carries domain meaning that benefits from a named partial, inlining can make the code harder to understand than the performance gain is worth.
The right question is not "are partials slow?" The better question is "is this particular view tree deeper than it needs to be for the amount of value that depth buys us?"
The helper trap
The helper experiment is especially useful because it catches a common instinct. It is tempting to think that moving HTML into a Ruby helper gives you the best of both worlds. You avoid partial rendering and keep a named unit of reuse.
It might look like this:
module InvoicesHelper
def invoice_row(invoice)
tag.article(class: "invoice-row") do
tag.strong(invoice.number) +
tag.span(number_to_currency(invoice.total_cents / 100.0)) +
tag.time(invoice.issued_on.to_fs(:long), datetime: invoice.issued_on.iso8601)
end
end
end
Then the view becomes:
<h1>Invoices</h1>
<% @invoices.each do |invoice| %>
<%= invoice_row(invoice) %>
<% end %>
It reads nicely at first. Then you profile it.
Helpers that build HTML with tag or content_tag still allocate strings, create safe buffers, escape values, validate tag names, process attributes, and capture blocks. Those operations are correct and useful. They are also work. Do them thousands of times in a loop and they add up.
This is the part I would keep taped to the side of my monitor: "Ruby helper" does not automatically mean "fast view component." Sometimes it just means you moved the same cost into a place that is harder to edit as HTML.
Profile before you flatten everything
The worst lesson to take from this benchmark would be "never use partials." That is how you end up with enormous templates that no one wants to touch.
The useful lesson is more boring and more durable: keep your view tree shallow unless deeper structure is paying rent.
Start with readability. Use partials when they name a real concept, isolate a repeated shape, or make the template easier to scan. Use collection rendering for repeated partials. Be suspicious of loops that call render over and over. Avoid helper-generated HTML as a performance hack unless you have measured it. Reach for caching when repeated markup is expensive but stable. And when a page feels slow, profile before rearranging the furniture.
Rack Mini Profiler, Scout, New Relic, AppSignal, Skylight, ruby-prof, and plain ActiveSupport::Notifications can all help you find where time is actually going. The view layer might be the culprit. It might also be a database query, an N+1 association, a third-party script, an image payload, or a product decision that asks the page to render far too much at once.
Performance work is useful only when it is attached to a bottleneck. Otherwise it is just interior decorating with benchmarks.
A practical rule for Rails teams
Here is the rule I would use on a real Rails app: Prefer readable templates first. When rendering a collection, use collection rendering. When profiling shows a hot repeated partial, consider inlining the small hot path or caching it. When a helper starts returning big chunks of HTML, ask whether it would be clearer as ERB. When a view has seven levels of partials, ask which layers are buying clarity and which layers are only buying indirection.
Rails gives us a beautiful default: write the obvious code, then optimize the parts that prove they need it. View rendering performance fits that philosophy nicely. Do not flatten every view because one benchmark crowned inline ERB. Do not ignore rendering cost because partials feel elegant. Measure the page, keep the tree honest, and choose the simplest thing that makes the user experience better.
Happy profiling!