相关文章推荐

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)
 
推荐文章