
Faster Rails partial rendering and caching. 78% improvement of test application - vm
http://ninjasandrobots.com/rails-faster-partial-rendering-and-caching/
======
joevandyk
It's always annoyed me how slow Ruby/Rails is at rendering views and partials.
I don't know why, but some of our small partials that don't do any network
calls take a long time to render. Profiling them doesn't really seem to help.
Possibly it's GC related.

If template rendering was faster, it wouldn't be as necessary to worry about
caching, which brings tons of headaches and complexity.

~~~
steveklabnik
If you're rendering a bunch of partials in a loop, you should try rendering it
as a collection instead; it's significantly faster.

~~~
netghost
I know I should just go read the code, but any insight into what the
difference is?

~~~
steveklabnik
Rather than all the setup and teardown work that's done for every single
partial, that work can be done once for the entire collection.

~~~
netghost
Thanks Steve, that makes perfect sense.

------
nthj
For what it's worth, I just implemented this on the staging environment of
<https://www.biglittlepond.com>. The one-line `render` call for the most
recently collected items dropped from ~700 ms to ~50 ms. 25 items per page.
This will be going into the production release later this week.

(This doesn't affect every page load, because I was caching the entire
rendered view, as well, but for cache-rebuilds when an item is changed or
added, this is -stellar-). Thanks, Nate.

~~~
nate
Yay! That's awesome. Thanks for that.

~~~
nthj
Sure! I should note, too, I was already caching each individual item, then
pulling in each cache one-by-one. The above improvements were only due to your
gem.

Also, I submitted a pull request [1] to add support for cache options, like
"expires_in: 3.days".

That's the only thing I could think to add. This is one of those magic
libraries that you're just like, man, did that really happen? Everybody should
use this.

[1] <https://github.com/n8/multi_fetch_fragments/pull/4>

------
sokrates
This reminds me of a talk about Evolution of Code at Facebook [1]. One of the
first architectural restructurings (and still the major driving factor behind
later changes) was the switch from sequential memcache_get to parallel
memcache_multiget.

[1]: [http://www.infoq.com/presentations/Evolution-of-Code-
Design-...](http://www.infoq.com/presentations/Evolution-of-Code-Design-at-
Facebook)

~~~
nate
Oh, thanks for sharing that. This presentation is great and is food for a
couple more ideas I think I might be able to pull off for Rails using multi
fetching.

------
tomfakes
I really like this technique. I've used it in a number of places, and it
really does give great performance.

My usage has never been as clean looking as this though.

It's always been a bit surprising that Rails doesn't use the multi_get caching
calls very much. These can be key to getting great results.

------
lukes386
Correct me if I'm wrong, but isn't 10 dynos to serve ~10 requests / second
pretty excessive?

The gem looks great and kudos to the author for getting such a great
improvement in responsiveness. I'm just a bit confused why he would choose to
set up the benchmark the way he did.

------
khangtoh
It still amazes me that devs are pulling hair trying to save a couple hundreds
of ms here and there but ignores and disses the view rendering time Rails
suffers from.

------
jherdman
Looks interesting. Has a pull request been sent to Rails to incorporate these
changes directly?

~~~
nate
No, but if I'm not mistaken they typically like to see new features shake
themselves out as gems. Like turbolinks or cache_digests. Right?

~~~
jherdman
Definitely cache_digests, Turbolinks seems to have jumped the queue though.
Either way, I'm excited to see some real world numbers in my app.

------
nachteilig
This has finally made me use collections appropriately, and man does it make
things fast.

------
habosa
I normally don't like HN negativity, but can someone play skeptic here and
tell me why this probably won't give me 78% partial rendering performance
gains in a real app? Or if it will, why this isn't standard?

~~~
nate
I can totally play skeptic :)

So like I mentioned in the description it really all depends. If your cache
store is already really close to your application like on the same server, you
aren't likely to see much gain since fetching from Memcached doesn't even go
across the network. But using something like Heroku where your Memcached
server might even be on another network that's not Amazon's you'll see some
nice benefit not having to connect to that Memcached server sequentially.

Also I rendered out 50 items from Memcached in the test app. Your use case
might be a lot less, or even a lot more.

------
atomical
Would template rendering benefit from a C extension like JSON encoding does?

~~~
steveklabnik
It's not as simple as "write it in C." There's an overhead involved in
switching from Rubyland to C land, and that can be significant.

------
eLobato
As a guy working on a Rails app that takes 5000ms avg to render a partial,
this is likely going into production now.

------
atomical
How is the cache being invalidated in this example when a model changes?

~~~
cliftonk
Rails, by default, uses '<table-name>-<id>-<updated_at>' for the cache key of
an activerecord object. When an ActiveRecord object's `updated_at` column is
touched, then all of it's cache keys will invalidate. I don't believe there's
a default implementation for a collection.

There's significant network overhead to looping over a collection of objects
and using cache blocks, so this gem appears to be a big win by using
memcached's `multi_get` command.

~~~
nate
Thanks for answering that. Yep, there's a bit here:

[https://github.com/n8/multi_fetch_fragments/blob/master/lib/...](https://github.com/n8/multi_fetch_fragments/blob/master/lib/multi_fetch_fragments.rb)

@collection.each do |item| key = @options[:cache].is_a?(Proc) ?
@options[:cache].call(item) : item expanded_key =
ActiveSupport::Cache.expand_cache_key(key)
keys_to_collection_map[expanded_key] = item end

Where I use ActiveSupport::Cache.expand_cache_key to create a key based on the
item or the Proc passed in. And expand_cache_key does the work of coming up
with the proper key. And activerecord objects have a default cache_key
implemented that uses an updated_at timestamp if it's available:

[http://api.rubyonrails.org/classes/ActiveRecord/Integration....](http://api.rubyonrails.org/classes/ActiveRecord/Integration.html#method-
i-cache_key)

~~~
cliftonk
Another thing to note is that typical memcached configurations have a short
max key length (256 or 512 or something like that).

------
hayksaakian
Why not contribute this to the rails standard package?

~~~
nate
I'd love for it to be part of standard rails, but don't they usually like to
see features like this play themselves out as gems before trying to
incorporate them (e.g. turbolinks, cache-digests, etc.).

Of course if anyone knows of a better way, please don't hesitate to let me
know or ping someone on rails core.

~~~
steveklabnik
As the newest Rails committer, I'd suggest that you post to the rails-core
list to discuss it, that's how things would work.

I haven't looked into your implementation, but if it just makes things faster,
and doesn't change semantics, then rolling it right into Rails is feasible.
It's new features-semantics that are generally 'new gems.'

~~~
nate
Thanks Steve, I'll do that. All it does is add an extra option (or two) to the
views render method. By default nothing changes. Stuff only gets invoked if
someone wants to do:

render partial: thing, collection: @things, cache: true

~~~
steveklabnik
Hmm, yeah, then it might be gem-worthy. Can't hurt to talk about it on the
list!

