Make ActionView Faster

It’s not unusual for template rendering to take longer than controller code. But you may think that you can’t do much to speed it up. Most templates are just a collection of calls to rendering helper functions that you didn’t write and can’t really optimize—except when they’re called in a loop.

Rendering is basically a string manipulation. As we already know, that takes both CPU time and memory. In a loop we multiply the effect of what is already slow. So every time you iterate over a large dataset in a template, see whether you can optimize it.

Rails template rendering has performance characteristics similar to Ruby iterators. It’s fine to do just about anything, until you render partials in a loop. There are two reasons for that.

First, rendering comes at a cost. It takes time to initialize the view object, compute the execution context, and pass the required variables. So every partial that you render in a loop should be your first suspect for poor performance.

Second, the majority of Rails view helpers are iterator-unsafe. One call to link_to will not slow you down, but a thousand of them will.

These two potential performance problems are really the same as we have already discussed in Avoid Iterators That Create Additional Objects and Watch for Iterator-Unsafe Ruby Standard Library Functions, just one level of abstraction higher, and they apply only to Rails. So let’s discuss these problems in detail and see what we can optimize.

Render Partials in a Loop Faster

When asked to render a set of objects, your template code would probably look something like this:

 
<% objects.each ​do​ |object| %>
 
<%= render partial: ​'object'​, locals: { object: object } %>
 
<% ​end​ %>

There’s nothing wrong with the code, except that it becomes slow on a large collection of objects. How slow? I measured the rendering of 10,000 empty partials in different versions of Rails and the results were not pleasant.


Table 2. Time to render 10,000 partials

Measured with GC disabled, in seconds. Results scale linearly with the number of partials.

Rails 2.xRails 3.xRails 4.x
0.335 ± 0.0061.355 ± 0.0331.840 ± 0.045

Although 10,000 objects is not a large dataset, just rendering the placeholders for them will set you back by 2 seconds with recent Rails. That’s disturbing. Also disturbing is that rendering also gets much worse with each subsequent version of Rails. But before you fall into your memories of good old Rails 2.x times, let me point out that even 0.3 seconds for doing nothing is already too much.

Rails 3.0 and higher has a solution to this problem called render collection:

 
<%= render :partial => ​'object'​, :collection => @objects %>

Or, in a shorter notation:

 
<%= render @objects %>

This inserts a partial for each member of the collection, automatically figuring out the partial name and passing the local variable.[5] That also performs 20 times faster.


Table 3. Time to render a collection of 10,000 objects

Empty partials. Measured with GC disabled, in seconds. Results scale linearly with the number of partials.

Rails 3.xRails 4.x
0.066 ± 0.0010.100 ± 0.005

The reason rendering a collection is faster is that it initializes the template only once. Then it reuses the same template to render all objects from the collection. Rendering 10,000 partials in a loop will have to repeat the initialization 10,000 times.

How much work is it to initialize the template? I have profiled the rendering of 10,000 partials in Rails 4 to illustrate that. Let’s look at the summary.

Operation

Percent of total execution time

Logging45%
Finding and reading the template (from disk or cache)21%
Setting up execution context (local variables, etc.)9%
Template class instantiation5%
Rendering5%
Other work15%

I’m sure we are both having our aha moment now. Actual rendering takes only 5% of the time. No wonder that if we skip initialization, we’ll get two orders of magnitude speedup—exactly as in our measurements.

Joe asks:
Joe asks:
But Rails Applications Rarely Need to Render 10,000 Partials, Do They?

Most likely not a lot of them render 10,000 partials. But 1,000 does not seem like an unreachable number. Let’s do some math. Imagine you render 100 objects with a partial in a loop. Now imagine that partial calls 10 other partials. These numbers look legit. If you render a paginated table with 10 columns, you’ll get a setup like this. How many render partial calls do we have? Already 1,000. How much time will we spend just inside the render partial function? About 200 ms according to my measurements. If we factor in the time for actually rendering useful content, we’ll easily cross the 1-second response time mark. And that’s already unacceptable for any web application.

Let’s see why logging takes 45% of the time. It turns out that with default config.log_level = :info in production mode Rails produces too much output.

 
INFO --: Started GET "/test" for 127.0.0.1 at 2014-08-13 10:21:40 -0500
 
INFO --: Processing by TestController#index as HTML
 
INFO --: Rendered test/_object.html.erb (0.1ms)
 
9998 more object.html.erb partial rendering notifications
 
INFO --: Rendered test/_object.html.erb (0.0ms)
 
INFO --: Rendered test/_dummy.html.erb (1904.0ms)
 
INFO --: Rendered test/index.html.erb within layouts/application (1945.4ms)
 
INFO --: Completed 200 OK in 1952ms (Views: 1948.6ms | ActiveRecord: 0.0ms)

Chances are you won’t want to silence your logs completely with config.log_level = :warn, but doing that would give you two times speedup.


Table 4. Time to render a collection of 10,000 objects with different log levels

Rails 4. Empty partials. Measured with GC disabled, in seconds

config.log_level = :info

config.log_level = :warn

1.840 ± 0.0450.830 ± 0.049

That is still not as fast as render collection (0.1 s). Where do the remaining 0.7 seconds go? It turns out that Rails implements a logger using the Observer pattern. Partial rendering triggers a render_partial.action_view event. When that happens ActionSupport::LogSubscriber gets notified and, in turn, runs Logger to produce the output. This plumbing code takes about 0.2 seconds. Template initialization and execution context evaluation take the rest.

Render collection has none of that overhead, and it doesn’t produce excessive log output, either. That makes it clearly superior to rendering partials in a loop.

There’s no render collection in Rails 2.x. But if you’re still using that version, try the template inliner plug-in.[6] It achieves the same effect by textually inserting partial code into the parent template before Rails compiles it.

This is what I wrote when I worked at Acunote,[7] the online project management system built with Ruby on Rails. There we rendered hundreds of tasks on the page, each task having 8--10 fields. For each field we had a separate partial for rendering, and there was no render collection in Rails 2.x. That’s when the template inliner was born.

To use it, add the plug-in to your Rails application, and append inline: true to the render statement:

 
<% @objects.each ​do​ |object| %>
 
<%= render partial: ​'object'​, locals: { object: object }, inline: true %>
 
<% ​end​ %>

Rails never sees the render partial call, and as result, we get the same two orders of magnitude performance improvement.


Table 5. Time to render 10,000 partials inline

Measured with GC disabled, in seconds. Results scale linearly with the number of partials.

Rails 2.xRails 2.x with template inliner
0.335 ± 0.0060.003 ± 0.0001

Avoid Iterator Unsafe Helpers and Functions

All rendering helpers are what I call iterator unsafe. They take both time and memory, so be careful when using them in a loop, especially with link_to, url_for, and img_tag.

I do not have any better advice than to be careful, for two reasons. First, you cannot avoid using these helpers (especially in newer Rails). Second, it’s very hard to benchmark them. Helpers’ performance depends on too many factors, making any synthetic benchmark useless. For example, link_to and url_for get slower when the complexity of your routing increases. And img_tag performs worse as you add more assets. In one application it’s safe to render a thousand URLs in the loop, whereas in another it’s not. So…be careful.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.254.118