In this three-part series, I will present simple tweaks you can make to your Rails app that each take about 5 minutes. These tweaks will improve the performance of your app, your productivity while developing it, and your sanity through avoiding future headaches. In this first part, we’ll talk about performance and how to speed up a sluggish Rails app.
1. (Properly) Index your Database
Indexes are a trade-off between space and time. Like the index of a book, they take up space, but when you go to look up “chimpanzee”, you don’t need to flip through each page to find it. You simply find it in the index and turn to the page it references.
As a rule of thumb, you should index any field that’s used to look up an object.
Foreign keys are the low-hanging fruit of the index world. There’s really no excuse to not index your foreign keys. In the world of Rails, foreign keys are the fields that look like othertableid and are used to join models together through relationships. (pro tip: this also includes the two id fields in a join table for HABTM relationships)
Another target for indexing are non-id fields that are used in queries. For example, say I have a Contact model with attributes: id, name and phone number. Due to the nature of our application, contacts need to be queried by phone number, not only by id. In this case, there might be the following line in the code
Contact.find_by(phone_number: “8885551234”). We need to send the database a memo telling it to optimize itself to be able to look up contacts using their phone number. We do this by indexing the phone_number field.
In ActiveRecord, you do this with a migration:
class AddIndexToContacts < ActiveRecord::Migration def change add_index :contacts, :phone_number end end
Now, when we look up a contact by its phone number, the database knows exactly where to look to locate that contact and can return it in a fraction of the time.
2. Leverage Caching
Caching parts of your application that don’t change often is a great way to speed up a user’s interaction with your website. ActiveRecord’s cache can store queries in memory, taking a load off the database. Since it’s easier and more cost effective to replicate your web server across many instances than it is to scale your database, caching can be a great performance booster.
Let’s say you have a shop built on Rails. When a user visits your store, they request the Product#index route. This route fetches Product.all from the database. Since you only update your product offerings occasionally, it’s unnecessary to hit the database every time a user comes to your site. In this case, we can cache our product index and specify a condition which invalidates the cache of products.
def index @products = Product.all fresh_when last_modified: @products.maximum(:updated_at) end def show @product = Product.find(params[:id]) fresh_when @product end
In the product index, we explicitly specify that the cache should expire when the lastmodified time is no longer equal to the last updatedat time of our products. In the show action, we simply use:
fresh_when @product which is a shortcut for caching the product until its updated_at time changes.
Caching can be a double-edged sword. The more complex a database query is, the more it stands to gain from being cached. Yet as the complexity of the cached query grows, so does the complexity of the conditions at which the cache should be invalidated.
Caches should be used sparingly and with forethought, but, when used correctly, can reduce the load on your database. This in turn speeds up your users’ interaction with the site.
3. Use Bullet to improve code performance
As your application grows and your models and their relationships become more and more complex, one of the biggest sink-holes for performance is the N+1 query. You might not be familiar with the term, but I’m sure you’ve seen (or even written) plenty of code that looks like this:
class PostsController < ApplicationController def index @posts = Post.all end end
<% @posts.each do |post| %> This post has <%= post.comments.count %> comments. <% end %>
This is an extremely common pattern in Rails apps. Rails abstracts the database layer so thoroughly with ActiveRecord that oftentimes we don’t think about the database.
Unless you’ve used eager-loading to prepare the database for a query like the one above, Rails is going to perform a query to fetch the comment count for each article. If you have a lot of articles, this is extremely inefficient. Eager loading allows us to get all relevant comment associations up front with a single complex query to the database (and don’t feel too bad for the database. Assuming you’ve indexed it, databases are more than capable of handing complex queries).
While learning the intricacies of writing database-aware queries in Rails will take more than 5 minutes, Bullet is a gem you can install in 5 seconds and start getting immediate feedback on the performance of your code.
Bullet’s mission is to “increase your application’s performance by reducing the number of queries it makes”.
It does this by watching out for N+1 queries and areas where you should be using eager-loading, but aren’t. It will help you solve these issues by giving you actionable feedback like:
N+1 Query: PATH_INFO: /posts; model: Post => associations: [comments]· Add to your finder: :include => [:comments]
Clearly, we need to be including the comments relationship in our query.
Going back to our example above, we would need to be sure to do the following in the PostsController:
class PostsController < ApplicationController def index @posts = Post.all.includes(:comments) end end
This will pack information about each posts’ comments into the @posts instance variable. Now, when we iterate over those posts, the comments count will be available without another trip to the database.
4. Try a different web server
Depending on the nature of your application and its hosting environment, a change of web server can have a dramatic effect on the performance of your application.
Each web server leverages different concepts like threads, forked processes and C-compiled extensions to achieve higher levels of concurrency under different circumstances.
Needless to say, there isn’t one magic bullet web server, and I won’t go into the pros and cons of each one here, but I urge you to take the time and explore the various options.
Some web server’s I’ve had particular success working with include:
Swapping out your web server shouldn’t take more than 5 minutes. You might spend more time tweaking the config settings, but you can usually find pretty good copy-paste configurations that will get you up and running with a new server.
Also, you might even try a different flavor of Ruby, like JRuby or Rubinius. Both are optimized to run threaded code (making them a great match for a server like Puma which uses threads itself to handle concurrent requests). This part is obviously more than a 5 minute commitment, so I won’t go any further with it, but just be aware that there are other options out there than MRI Ruby.
5. Measure your performance
“What gets measured, gets managed.” - Peter Drucker
It’s important that as you make tweaks to your application’s code, settings and environment, that you understand the net effect of each change.
For comprehensive analysis of an application’s performance, nothing beats New Relic. Besides their hosted platform (which takes no time at all to set up and integrate), they also offer a free local version that you can run locally. I find this version more helpful for the tweaking stage, because I can try out changes to the code and immediately see their impact on response time of a method.
If you’re on the Heroku platform, there’s a nifty little feature called log2viz. This experimental tool provides a visual analysis of the last 60 seconds of log data for all your Heroku apps. This is a great way to get a pulse on the load your apps are under to see if more dynos are required.
Finally, Librato is a wonderful performance measurement tool that sits somewhere between the ephemerality of log2viz and the comprehensiveness of New Relic. I use it it get a deeper insight into how my application is behaving over time at more of a hardware level. It has some great built-in preset metrics if you add it as a Heroku add-on. My favorite is the Dyno Load Average which represents how over (or under) utilized your dynos are. An app is running optimally when it hovers around a 1.0 load average. Too much more, and your app will start seeming sluggish. Too much less and you’re over-paying for your hardware.
As a side note, load testing your application in conjunction with tools like New Relic, log2viz and Librato can quickly reveal the cracks in its armor. I’ve used tools like Blitz and loader.io, but neither offered enough flexibility for me. Instead, I opt for a more custom solution using Locust, a Python library that allows for fine-tuned replication of many users attacking your site at once. But again, this is outside the scope of our 5 minute tweak so I’ll leave its details to a later posts.
Make sure you’re testing your tweaks locally with a copy of your production database! This is huge. Most performance issues creep up as the records in your database increase. If you’re testing locally with a clean database, you won’t experience the same performance issues that your users are seeing in production. So grab a copy of your production and import it into your dev database. This ensures that the queries you’re tweaking are as close to real life as possible.
That’s it for Rails Tweaks Part 1: Performance. Stay tuned for Part 2: Productivity!