Performance is a topic that’s widely discussed and also, sadly, widely misunderstood.
I’m going to share five performance tips that have worked well for me in my 9+ years of using Rails and even before that, since much of what’s here is technology-agnostic.
I want to add a caveat: I’ve never worked for a big B2C startup that has needed to scale to millions of users. I’ve worked mostly applications with modest user bases and modest performance requirements. Luckily, I imagine my career experiences are probably pretty similar to that of most Rails developers, so it’s highly likely that what has worked for me performance-wise will also worked for you.
If you already know the basics of performance optimization and want to hear from someone who does Rails performance work all day every day, I recommend checking out the work of my friend Nate Berkopec, who I know as “The Rails Performance Guy”. (I’ve also had Nate on my podcast.)
Now onto my tips.
1. Don’t optimize prematurely
Before I came to Rails I worked with PHP. At one of my PHP jobs, I worked at a place where our official policy was to use single-quoted strings instead of double-quoted strings. The reason for this policy was that, supposedly, single-quoted strings were parsed ever so slightly faster than double-quoted strings.
In other words, someone in leadership took great care to implement and enforce a policy that was probably responsible for like 0.0001% of the total performance picture for any one of our apps. Penny wise, pound foolish.
Don’t do this. When you’re first building an application, the right approach to performance 99% of the time is to not think about it at all. Unlike bugs, which are best never deployed to production, performance problems are usually best dealt with after they’re discovered in production, after they prove themselves to be legitimate problems.
2. Profile and benchmark before optimizing
When I talk about performance profiling, I mean taking stock of performance across an application and judging where optimization effort is best spent. Some judgment and common sense come into the picture here. For example, if two pages are equally slow, and one of them is visited 100 times a day and the other one 1 time a month, obviously fixing the busier page will give a higher ROI.
In practice I rarely bother to do any actual profiling though. This is partly due to the nature of the work I personally have done historically. Most of my projects have been back-office internal-facing apps where I have a direct line of communication with the users or someone representative of the users. If there’s an important page that’s unacceptably slow, I’ll usually hear complaints about it or I’ll observe the sluggishness firsthand.
The next step I take before setting to work on optimization is to get a benchmark. This is just a fancy way of saying that I measure how fast the page currently loads. I of course can’t know how much of an improvement I made unless I know the difference between the “before” speed and the “after” speed.
The way I typically perform my benchmarking is to simply load a page in my browser while watching the output of the logs in a different window. At the bottom of the log it will say, e.g. “Completed 200 OK in 760ms”. I also use the sql_queries_count gem because sometimes it’s helpful to be aware of how many queries are being executed on the page.
3. The key to performance is usually not to add clever things but to remove stupid things
Performance optimization is often more akin to digging a ditch than to solving a Rubik’s cube.
Typically, the thing responsible for most of the performance cost of a page is waste. There’s a thing that could easily be done in an efficient way, but for whatever reason it was done in an inefficient way. So the task when optimizing a page is to find the waste and remove it. More on this in the next tip.
4. It’s almost always the database
The cause of most performance problems is waste. Most waste takes the form of making too many trips to the database.
This fact is so true and important that, in my 15+ years of programming experience, almost every performance optimization I’ve made has been some variation of “hit the database less”.
In Rails, the most common form of hitting the database too much is the famous “N+1 query problem”. Let’s say you have a page that shows a list of 10 things—let’s say clients—where each list item shows not only the client’s name but the client’s address. This means that a database query occurs for each one of those 10 clients. That’s the N in N+1. The +1 part of N+1 is the original query behind Client.all
that grabs all the clients in the first place.
A common solution to the N+1 query problem, one I use a lot is eager loading. Instead of doing Client.limit(10)
, you can do Client.includes(:address).limit(10)
, which will grab all associated addresses ahead of time in just one query instead of in 10 separate queries. So instead of a total of 11 queries (1 for all clients, 1 for each of 10 addresses) you’ll have a total of just 2 queries (1 for all clients, 1 for all addresses).
Another way to make fewer trips to the database is to cache the results of expensive queries. I often use Rails’ low-level caching for this. However, this adds a certain amount of complexity that I’m not always eager to add. So I prefer to try to make fewer trips to the databases and/or optimize my queries themselves, resorting to caching only if I have to.
5. If it seems faster, it is faster
The only thing that ultimately matters is how fast users perceive the application to be. If a certain change makes an application feel faster, then for all intents and purposes, it is faster.
I once heard a story about a condo where the residents complained about the elevators being slow. Management’s reaction was not to immediately try to speed up the elevators somehow, which would presumably have been difficult and expensive. Instead, management installed mirrors on the walls next to the elevators. The complaints went away.
So, sometimes the right initial response to a performance problem is not necessarily to embark upon a big, expensive optimization project. Sometimes a change can be introduced that’s less invasive but still effective. For example, let’s say I have a calendar page with back/forward buttons to go from month to month, and let’s say each month takes 800ms to load. If I click the button and nothing at all happens for 800ms, it might feel a little slow. But if I change it so that immediately upon clicking the button I see an animated loading indicator, the page will feel faster. It sounds dumb but it’s absolutely true. Sometimes those little changes that improve the responsiveness of a page will increase its perceived performance, without needing to touch the actual performance at all.
6. Bonus tip: it’s almost never Ruby
Despite what you might read online about Ruby being “slow”, I’ve never ever encountered a performance problem where the culprit was the Ruby language itself. Whether an application is written in Ruby, Go, Python or whatever else, the performance bottleneck is almost always the database.
If all you know about performance is the five tips above, you’ll probably be able to tackle 80% of the performance challenges you encounter.
I couldn’t agree more on all points. I bookmarked this page so that I can just send a link to this page. 🙂
Thanks!