Sub One-Second Page Loads with Rails
Delight your users with pages that pop!
Like it or not, the speed a webpage loads has a major impact on how likely people are to spend time on your site. Obviously, a snappy user experience isn’t everything (useful content will always be king) but a sluggish load time is very likely to make them close the tab and forget all about you.
Modern web frameworks like Rails make life a lot easier when producing complex web apps. The syntax is clean, the expressions easy to read, and the framework handles a lot of the boilerplate and drudgery in an unobtrusive and intuitive way. However, many critics would say that the price you pay for the convenience of such tools is that your web app is doomed to be sloooooow….
And (I’m sorry to admit) there’s some truth in it.
Why is Ruby considered slow?
Ruby is a language optimised to make programmers happy, but machines have a lot of hoops to jump through in order to run code written in it. Compared to compiled languages, it’ll always have the interpreter overhead; as a dynamic language, the interpreter has a lot of additional work to do at runtime to determine the type of data it’s dealing with; the garbage collection still affects runtime a great deal; and Ruby (at least, the standard MRI implementation) doesn’t support true thread concurrency.
Luckily for us, none of those points are show stoppers for one major reason:
The web is (still) a slow place, with network latency and available bandwidth being the major bottlenecks for load times. In practice, the server request/response section of the page load where Ruby actually has to do some work is typically less than a quarter of the overall page load time.
What actually happens when a page loads?
Well, a lot of things…
Worst case scenario, with zero cached data and pessimistic load times:
- DNS lookup for the domain (200 ms).
- TCP handshake with the web server (100ms).
- Send the page request to the web server. At this point, the Rails router receives the request and decides what to do with it, hopefully passing it to one of your controllers (10ms).
- Rails asks the database for some data, and waits for the response (200ms).
- Rails renders the data into a template and generates a document to send back (100ms).
- The browser receives the document via the established HTTP connection (500ms).
At this point, we’re already past the 1-second mark and we’ve only just finished retrieving the document. Next, the browser needs to:
Clearly, there’s a lot of room for improvement here…
Chrome Developer View
To see all this in action, I recommend installing Chrome, if you don’t already have it and checking out the Network section of the developer tools (View > Developer > Developer Tools). Click the red record button and then load a page to see what’s happening behind the scenes.
Using Chrome’s dev tools, you can see where the pain points are in your app’s page load. With the following pointers, hopefully you can make some gains and shave off some time.
We’ll tackle the optimisations in two broad sections: server side, and client side.
Server Side: Optimise your Rails app
- Usually, the slowest part of a web request on the server side is the part where the app communicates with the data store. Assuming your Rails app is fairly typical, you’ll be using a relational database. This means you need to try and make the numbers of SQL queries per page load as small as possible. One good approach is to use the ActiveRecord::QueryMethods#includes() method to load dependent relations in one go.
- There’s a great tool called bullet, which is designed to find situations where eager loading could save database transactions. It’s well worth trying out in your app to see where you could make some gains.
- To profile where in your code your app is spending most of its time, use the excellent Rubyprof tool. Large numbers of calls to specific methods, or a long time spent in those methods, could mean unnecessary work is being done and reveal some potential performance gains you can make in the code.
- If you have code that is unavoidably slow (lots of computations, or external API calls) then make it into a background job with ActiveJob and use the Rails cache or the DB to store the results. That way the user never has that server wait time added to their page load time. Getting a balance between cache freshness and page load speed is a bit of an art but keep tweaking it.
- Use Unicorn as your app server and set 1 worker per CPU core on your server. Use the app preloading feature, because it loads the app into the master process which can then pass the loaded data to each worker without it needing to set up its own copy. Unicorn’s an excellent piece of software, and the way it works internally is well worth reading about in this easy-to-follow post.
- In front of Unicorn, use something fast and configurable like NginX as the main web server and have it pass requests upstream to Unicorn like so:
- Keep Ruby up to date. Large (and small) strides with garbage collection and other internals produce performance improvements every few months. Same story with Rails — performance is being improved all the time, with particular effort recently going into ActiveRecord to make database interactions more efficient.
Client Side: Optimise your page loads
<script src=”//example.com/script.js” async></script>
- You can pay to use a CDN (Content Delivery Network), or possibly use a different asset host to reduce requests to your app server (the fastest web request is one that doesn’t have to touch your app server at all!). You can set up a separate assets host using only NginX to host your asset files. Just install it with the Linux package manager, and set the root directory in NginX to use the location where your compiled assets are. This is quite easy to slot into the Rails asset pipeline.
- Make sure your assets server is set to gzip your assets. This reduces the amount of data sent over the network (files are unzipped very quickly once downloaded).
- Make sure your assets server sets cache headers so your browser knows it can cache them and how long for. All subsequent page loads should be faster because the browser can safely load your asset files from disk.
Due to the way the asset pipeline is designed, when you update your CS or JS (or any asset) it’ll lead to new asset files being created with a digest of their contents in the file name — this means the browser won’t erroneously show an old version of an asset once a new one has been created.
- Use HTML5 preconnect hints to tell the browser about third party servers the page refers to. This’ll allow it to decide to warm up a TCP handshake early in the page load sequence so when it comes to time to make the request, there’s no wait time for the connection to be established.
<link crossorigin href=”http://anotherserver.com" rel=”preconnect”>
- A large application.js or application.css file.
- Most large third party libraries let you pick which components you need (jQuery UI, Bootstrap etc) so make sure you’re using a trimmed down version for the fastest page loads possible.
Putting it all together
Now you’ve made some tweaks, see how your Rails site performs with Google’s free analysis tool, Page Speed Insights. This’ll offer you up more hints as you work through it.
So there we go! Nothing revolutionary, but each step here should hopefully help shave off some loading time and make things feel a lot snappier.
- How Google Chrome speeds up your page loads: https://www.igvita.com/posa/high-performance-networking-in-google-chrome/
- How Unicorn Leverages Unix for Speed: http://thorstenball.com/blog/2014/11/20/unicorn-unix-magic-tricks/
- Really get your hands dirty with Chrome page profiling: https://developer.chrome.com/devtools/docs/timeline