Server side caching is a great way to dramatically speed up the serving of your pages. The easiest way to implement caching in your rails app is to use key-based fragment caching.
For those unfamiliar with this concept, take a look at
How key-based cache expiration works
on 37signals blog.
The all-new-basecamp (kinda Apple'ish, but I like to call it that:) utilizes this technique heavily. As a result they have page lads faster than the blink of the eye, and who wouldn’t like that? Since pages are served much faster, web server can serve much more visitors (lower hosting price). On the other hand, you’ll need
huge amount of RAM
, which will probably compensate for the former benefit.
Snappy page loads remove the need for
custom
ajax, which simplifies the app’s internals greatly, which in turn makes it easier to test.
You start developing your app with this approach, and everything works great and fast until you read the following on your
feature list
:
Display unread messages in bold
Although your first idea may be to wrap the cached fragment with a div tag and conditionally apply
unread
attribute on the wrapper. This could work in most simple cases, but it is wrong:
-
n+1 queries
are likely to happen - unless you use association preloading which i really hate since it hurts my OOP fealings (more on that in future post:)
-
Russian doll caching
wont work
-
wrapping things with
div
s can’t be a good thing, right? :)
I refer to this problem as
cache personalization
, and there are 2 ways it can be handled.
Client-side cache personalization
This is usually the best option (if it is possible for given scenario). Lets consider this request:
Replace username with
you
for current user
This can be solved fairly easily. Instead of simply outputing the user’s name, we can output this:
<span class="username" data-id="4">zogash</span>
Later we can create simple javascript that could take all usernames, and if the
data-id
matches the
id
of the current user (which we can store as meta tag or something), then replace it with the “you”. We can even apply some classes to it and style it in different way. So, after being processed with some js, the previous snippet should look like:
<span class="username current" data-id="4">you</span>
However, when dealing with more complex requirements, which, for example, involve querying the database, then we must fallback to server-side cache personalization.
Server-side cache personalization
If you can’t make it work on client side, you can always resort to your all-mighty-and-powerful server. Even though following may smell like a wrong thing to do, please stick with me and see all the benefits of this approach.
The general idea is to have some kind of
response filter
. A thing that would take the generated HTML, manipulate it in a way (like we do with jQuery), and return the outcome to the user.
We’re doing this all the time with jQuery, why can’t we do it on the server too?
.
Ok, so first, how do we get the generated HTML? The simplest solution is to hook
after_filter
to your
ApplicationController
. It would look something like this:
class ApplicationController < ActionController::Base
after_filter ResponseFilter,
if: lambda { |c| c.response.content_type == 'text/html' }
# ...
Idealy you should create a
Rack
app for this purpose, but for simplicity sake, lets stick with
after_filter
.
Now, we need to define the
ResponseFilter
class. If you refer to the docs it needs to implement the
filter
class method which retrieves the
controller
object as only argument. You can access the
controller.response.body
and manipulate it in any way you want.
I won’t go into details here, but the previous example could be done on the server the same way we did it on client. The only difference is that we wouldn’t use jQuery to manipulate the DOM, but
nokogiri
to manipulate the html document.
One benefit of this approach is that
we can analyze the entire page, and take all we need from database table in one query
. For example, if there are 10 articles on the page, we can grab their ids from the DOM, fire single query to
readals
table to find out which ones are unread, then apply “unread” class on them.
I tend to organize these response filters as separate files in
/app/filters
directory. I have one special filter that acts as a collection and applies chain of filters on response. This proved to be very fast and efficient.
Special Case: Lazy Cache Evaluation
Lets consider the following cached html fragment:
<% cache ['v1', post] do %>
<article class="post">
<%= render post.author %>
<%= post.body %>
</article>
<% end %>
The author’s partial is also cached. It contains its avatar and name. Now,
what happens if user changes its avatar?
If we don’t include the author in the cache key, his old avatar will still be displayed in all posts. If we, however include the author in the cache key for the post, we’ll need to make 2 queries when calculating the cache key: one for the post, and one for the author. If we have multiple users displayed in an article (e.g. reviewer, publisher.. etc) this would tend to get messy. And if we start using russian doll caching concept, it would almost be impossible to track all this.
The concept i came up with can be called “lazy cache evaluation”. Instead rendering the author’s partial and including it inside the post’s cached fragment, we could use this instead:
<% cache ['v1', post] do %>
<article class="post">
<article class="user" data-id="<%= post.author_id %>" data-lazy />
<%= post.body %>
</article>
<% end %>
Now, we could create new response filter that would get all articles with
data-lazy
attribute and replace them with rendered partials. Once again, since user partials are also cached, render method would simply read those from cache. The benefit from this approach is that:
-
no need to specify user as part of cache key
-
smaller caches
-
all users mentioned on entire page loaded with single query
-
partial is rendered only once per user (and applied in several places)