Ruby is a fast language, and a great one in so many ways, but nothing in this world is truly free. It’s very easy to do things that seem inconsequential but that later can bring your application to a grinding halt. In this post, I’ll outline five important ways that you can avoid some of the most common problems Rails apps encounter.
Before continuing, a disclaimer: do not take these tips and refactor your code ad-hoc. Take everything with a grain of salt and perform your own measurements to determine which pieces of your app are slow. Before making any performance optimizations, get set up with a profiling tool, like RubyProf, New Relic, Scout, etc. You always want to know where the most significant bottlenecks are for you, and focus your efforts there first.
Eager Load Associations
The most common and significant problem that I’ve seen in Rails apps has been the lack of eager loaded associations. A simple extra _:include_
when performing ActiveRecord finds will prevent 1+N queries. So for example, if you are displaying a list of articles on your blog homepage and want to display the author’s name as well, load the posts with Post.all(:include => :author)
. For those complex pages, eager loading works multiple levels deep. Newer versions of ActiveRecord handle complex eager loading cases much more elegantly by splitting up a large join query into multiple smaller queries that make better sense.
Note: only perform the eager load when you actually plan to use the objects, because there’s fairly significant overhead to creating many ActiveRecord objects.
Do Database Work In the Database
In the same vein as the first tip, try leveraging the database when it makes sense. Relational databases are designed to query large amounts of data and return results; Ruby is not.
For example, if you want to check if the user currently logged in has commented on an article, you don’t need to load all the comments for that article. Iterate through each one, and check whether at least one comment was created by the current user. Doing this will instantiate objects for every single comment and then instantly discard them after the check is done. A much better way to obtain the same result is to push the logic to the database by doing a SELECT COUNT statement. ActiveRecord has an easy way to do this: Article.comments.count(:conditions => ['user_id = ?', current_user.id]) > 0
Do as Little as Possible During the HTTP Request Cycle
You want to be able to return a response to the end user’s request as quickly as possible, so only do the bare minimum needed to return the response and defer everything else. Actually sending out an email is relatively slow and users don’t generally care if emails are sent during the request cycle or right after.
Whether this is implemented using a simple Ruby thread or a robust, distributed queuing system like RabbitMQ doesn’t really matter. Rails 3 will ship with a default queuing system, but until then, I suggest checking out DelayedJob and BackgroundJob.
Know Your Gems and Plugins
As Rails applications get more complicated, a good thing to do is to use existing plugins and gems instead of recreating the work in house. This usually introduces a significant amount of new code to the application that is relatively unknown.
There are many great Rails plugins out there. But before depending on a new gem or plugin, I suggest at least skimming the source – check for any craziness. Also be sure you’re using plugins for their intended purposes – or things are likely to go awry.
Avoid Creating Unnecessary Objects
Every time Ruby’s garbage collector is triggered, Ruby will stop running your code and start cleaning up unused objects. This process can take between 100 and 400ms on MRI (JRuby has a better behaved, tunable garbage collector through the JVM), which is a noticeable period of time._ Avoid this as much as possible_. This means avoid creating unnecessary objects. I have already mentioned a couple of ways to do this in the previous tips.
In general, the best way to avoid the unnecessary creation of objects is to understand how Ruby and the libraries in use work. For example, understand the difference between these two snippets:
sentences.map { |s| s.strip }
sentences.each { |s| s.strip! }
The first snippet creates a new Array object and a new String object for each element in the Array. The second snippet just mutates the String objects in the Array without creating new Ruby objects.
Granted, this tip only makes a significant difference when dealing with large data structures, but it’s a good idea to keep in the back of your mind whether or not you actually need to duplicate objects. If you have arrays containing thousands of ActiveRecord objects and use reject
vs. reject!
, you’ve just created a second array which could potentially have thousands of objects.
There are many other aspects of a Ruby on Rails application that can cause bottlenecks; listing them all is obviously impossible. That said, the most important thing to learn is how to locate these bottlenecks. Solving them can be handled on a case by case basis.