Speeding Up Your Ruby on Rails App

You might already know this; Sidekiq is a pretty well-known gem.

But you don’t need Sidekiq to leverage threading; it’s built right into Ruby.

Threading won’t improve CPU-intensive tasks (Ruby doesn’t allow parallel execution of threads), but if you’ve got pages waiting on blocking I/O, it can make things run a lot more smoothly.

Maybe you’ve got a couple critical pages that more or less just wait on one or two HTTP requests before they become fully functional (perhaps they’re grabbing information from specific pages that will be different depending on what the user did).

You probably don’t need to install a gem for a simple case like that; you can implement threading yourself.

The syntax is quite simple:Thread.

new do <code here>endIf you need to get the value of the thread when it resolves (maybe you’ve scraped a page and you want to get the result when it’s finished), you call .

value on the end:def scrape_page(url) Thread.

new { Nokogiri::HTML(open(url)) }.

valueendAnd tada, you can open multiple pages simultaneously; the time it will take to retrieve information from all your HTTP requests will be equal to the wait on the slowest page, rather than their total!Serializing: Ditch .

to_json and AMSIf you’re only serializing small sets of data or single objects, this probably doesn’t matter.

Continue happily calling render json: your_data on whatever you’re sending to your front end.

However, if you find yourself serializing hundreds or thousands of objects, you should really switch your serialization engine.

Netflix’s Fast JSON API runs circles around Rails’ Active Model Serializers (AMS).

It can make a huge difference in page loads where lots of objects are being serialized.

On one page where I was serializing 1200 Ruby objects (later paginated and displayed with JS), my page load times went down from an unacceptable ~850ms to a much snappier ~200ms.

Fast JSON API leverages OJ (short for Optimized JSON), a JSON parser gem for Rails that has been around for a while now, so if all you need is JSON parsing (or something to replace the built-in .

to_json method), OJ is the way to go.

Lazy-Loading ContentIf you’ve got pages where there are a lot of off-screen images or videos, be you on Rails, Sinatra, Node, or Super Ninja Fire Penguin Ultra, you could likely benefit from lazy-loading.

We don’t really notice when we’re on our phones or laptops connected to a high-bandwidth, low-latency WiFi connection, but having dozens of images weighing in at hundreds of KB each adds up pretty quickly.

The problem is this: If you’ve got tons of offscreen media, a browser’s default behavior is to load them all.

If you’ve got a fast device and connection, that’s probably fine (if not ideal).

If you’re on a slower connection, or a data limited mobile device, this is less fine.

Loading several megabytes of images the user might not ever even see wastes data, bandwidth, processing power, and potentially battery life.

While the biggest impact will be felt on higher-latency, lower-powered devices, this can hurt page loads on fast machines too, and sometimes even a small speedup can be the difference in whether or not a page load feels immediate.

Rails’ options are somewhat limited, but I’ve found the lazyload-rails gem works for me in Rails 5.

2, even though it hasn’t been updated in a while.

This way, images won’t be loaded unless a user actually scrolls down far enough to see them.

Eager Loading: Kill N+1 QueriesIf you see a flood of queries in your console, all related to one single action and all looking suspiciously similar, you’ve probably got an N+1 issue; you’re executing one query to retrieve a parent object, and then N additional queries, one for each child object.

This is something you can easily miss in development if you aren’t using a test database that’s representative of the scale you’ll eventually be working on.

But once you hit a certain critical mass, it can sink you.

Example:Continuing with our Game of Thrones theme, let’s say we’ve got a House class which has_many people, and a Person class which belongs_to a house.

We’ve set @people = Person.

all in our controller action.

Then in our view for our index page, if we’re calling all of our people and displaying the house they belong to like this:<% @people.

each do | person | %> <div> <%= person.

house %> </div><% end %>…it will load every house one at a time.

meaning for every iteration in our loop, we’re sending another query to the database.

You can observe this in your server output, looking something like this:Person Load (40.

7ms) SELECT "person".

* FROM "people"House Load (0.

8ms) SELECT "houses".

* FROM "houses" WHERE "houses".

"id" =?.LIMIT 1 [["id", 1]]House Load (0.

7ms) SELECT "houses".

* FROM "houses" WHERE "houses".

"id" =?.LIMIT 1 [["id", 3]]House Load (0.

7ms) SELECT "houses".

* FROM "houses" WHERE "houses".

"id" =?.LIMIT 1 [["id", 5]]House Load.

On the one hand, we’re putting our database’s IOPS performance to the test, so that’s fun.

On the other hand, we’re slowing down our database and therefore our site, in addition to littering our production logs with thousands of lines that really shouldn’t be there.

This happens because ActiveRecord, like most ORMs, lazy loads everything by default, so until we actually ask for something we don’t get it.

We’re not asking about houses until we’re inside of a loop, so when we call Person.

all, it just loads the necessary Person data, not any associated records (like the house the person belongs to).

*The fix?.@people = Person.

all.

includes(:show)With this simple use of includes, we’ve eager loaded the associated house records, and so we only have to query the database twice; once for the Person model, and once for the House model.

We don’t actually end up loading less data, just hitting the database far fewer times.

Installing the Bullet gem will help you spot and take care of N+1 queries.

*This is of course a very good thing; when we set our variable for our show page, @house_frey = House.

find_by(name: "Frey"), we normally don’t want to load every single dead Frey along with it.

Upgrade Ruby ItselfThis one’s probably pretty obvious.

But if you’re still running Ruby 2.

0 (or 2.

2, or 2.

3.

) you’re missing out on a fair bit of performance.

Every Ruby version tends to bring speed improvements, often across the board, and this translates over to Rails performance — response times improve even in I/O bound applications.

And while Ruby 2.

6.

0’s JIT isn’t really ready for primetime yet when it comes to boosting Rails’ performance, there will be a time in the probably not-too-distant future when it will be.

New Ruby versions also bring specific improvements and new methods that are often faster for their specific use cases than less-specialized methods.

Ruby 2.

4, for example, came with a big performance boost for hashes, and gave us helpful methods like .

match?.(think JavaScript’s .

test) which, when all we want to do test a string against a Regex, is much faster than .

match.

It also gave us .

sum, which in addition to being pithier than .

reduce(:+), is also speedier.

Don’t Ditch Turbolinks!All right, I know Turbolinks can sometimes be a pain, and the headache might occasionally outweigh the benefits, perhaps especially in larger, more complex apps; if this is you (or if you’ve got a front end like React with client-side routing making Turbolinks useless anyway), I won’t argue the point.

But although it can cause issues we sometimes need to work around, it usually “just works” (in Rails 5+, anyway), and is worth using for the extra speed.

Turbolinks really does improve perceived smoothness for the user.

You can read here about its benefits and how it works if you remain unconvinced.

Compression (and minification)The internet wouldn’t work very well without compression.

(Actually, computers wouldn’t work well in general.

) Sure, we’ve got 100 Mbps+ internet connections, but good luck streaming uncompressed 1080p video on YouTube with that.

By default, when you precompile assets in Rails, you minify HTML, CSS, and at least attempt to minify JS.

Unfortunately, Uglifier, the de facto Rails JS compressor, doesn’t have ES6+ compression enabled by default yet.

Luckily however, support is in-built (it’s ‘experimental’, but I haven’t had any issues with it); all you have to do is make a small change in your production.

rb:config.

assets.

js_compressor = Uglifier.

new(harmony: true)And voila, you’ve got minified JavaScript.

If you have control over the assets displayed on your site, make sure they’re stored in efficient formats.

If possible, use SVGs where appropriate.

Serve JPEGs instead of PNGs if you don’t need image transparency.

(WebP is a more efficient option either way, but users will probably hate you if they have any interest in saving those images themselves, as WebP isn’t a format that plays particularly well with others.

)Finally, we can add gzip compression to files that haven’t already been compressed, with Rack::Deflater.

In /config/application.

rb add the line:config.

middleware.

insert_after ActionDispatch::Static, Rack::Deflater.

You can go here for an explanation about what it’s doing, and use this neat little tool to let you know what kind of compression ratios you’re getting.

This can potentially have a big impact on how fast your site is, especially for users on slower connections.

Final Thoughts: Gems, jQuery, .

count and CachingDon’t Overuse GemsWe’re going to need to use some gems; they provide flexible, extended functionality for our app saving us enormous amounts of time, and often doing things we probably couldn’t really do very well on our own anyway (gems like bCrypt, Devise and OAuth come to mind).

But if you add a gem every time you stub your toe, you’re adding a lot of overhead.

Gem-heavy apps can become unwieldy and memory hungry, which can hurt performance and make it harder to upgrade to newer Rails versions in the future due to dropped support or bugs.

Is there a Gem out there that solves a bunch of your problems all at once?.Great, go get it.

Do you have a small bit of functionality you want to add?.Maybe don’t add that huge gem that does 800 things when you’re only going to use one of them.

Perhaps find a more specialized gem, or simply add the feature yourself instead of loading a whole library to do the job for you.

jQueryIf you’re using jQuery in your Rails app, assuming you don’t need to support IE < 9, run jQuery 3.

x instead of jQuery 1.

x.

This is as simple as changing the //= require jquery (or require jquery.

min) in your application.

js file to //= require jquery3.

min.

You can use jQuery2 if you need to, too, but whichever you choose, both are faster and smaller than the original.

Use .

size, not .

countDon’t use .

count in a query unless there’s a specific reason to do so;.

size will choose the appropriate way to accomplish what you ask it to do.

And don’t use .

length to determine how many records there are; ActiveRecord doesn’t define a length property, so if you call, for example, Person.

all.

length instead of Person.

all.

size, all Person records get loaded and converted into an array, and you’ll end up with results like this:Person Load (346.

0ms) SELECT “people”.

* FROM “people”instead of this:(12.

0ms) SELECT COUNT(*) FROM “people”Ouch.

CachingCaching is a wide-ranging, thorny topic, and getting it right ranges from pretty straightforward to pretty bendy.

The idea behind caching Rails apps is the same idea behind caching in general; we save frequently-used pieces of data so we don’t have to constantly query a database or reach out to a network to retrieve the same information over and over.

Much of this is already done for us by browsers; a user’s browser will cache much of what it comes across, thereby shortening page loads for individual users.

Rails also automatically caches things for us, like our actual code, for instance.

However, there is still plenty that isn’t automatically cached, and failing to cache in production can lead to poor performance.

Because of the depth of the topic, we’re not going to get into specifics here, but check out the Rails guides on caching; pay particular attention to fragment caching, as it is the most common use-case.

Understanding caching in web development is also a helpful and beginner-friendly read.

That’s it! I hope you enjoyed reading, and learned something that will help you speed up your lagging Rails app.

 :).. More details

Leave a Reply