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:

  1. DNS lookup for the domain (200 ms).
  2. TCP handshake with the web server (100ms).
  3. 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).
  4. Rails asks the database for some data, and waits for the response (200ms).
  5. Rails renders the data into a template and generates a document to send back (100ms).
  6. 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:

  1. Parse the HTML document and find secondary resources to download (linked stylesheets, JavaScripts, images etc). Each of these will need a full DNS lookup, TCP handshake, and time to wait for the server to send the file. Let’s say 5 additional seconds).
  2. Once the browser has all referenced JavaScripts and stylesheets, it now needs to read them to determine how the JavaScript may need to change the page. (up to a second, if there’s a lot of JavaScript).
  3. Once the JavaScript has been evaluated, the DOM is ready and the CSS can be applied and rendered to the window (usually less than 200ms).

It’s at this point from a user’s perspective that the page has visibly finished loading. There may be other resources loaded behind the scenes, and actions carried out by JavaScript but at this point the user can see and interact with the document.

Phew!

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.

Chrome’s network view shows all the page assets and the time it took to load each over all

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

  • Load JavaSript files asynchronously. If you don’t, the browser stalls its parsing of the HTML just to download and evaluate the included JavaScript file. If you add the “async” attribute, the browser will download and execute it in parallel with parsing the rest of the document.
<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.
  • Make sure you minify your CSS and your JavaScript (this can compress over 50% easily and thus download faster).
  • 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”>
  • Keep your use of JavaScript and CSS to a bare minimum (aim for 100kb or less once minified). Rails already puts all JavaScript and CSS into single files and minifies them, which helps reduce additional round trips to fetch multiple scripts. However, if you use large third party libraries during the asset pipeline stage, it can cause two problems:
  1. A large application.js or application.css file.
  2. All the JavaScript needs to be evaluated fully by the browser before it can render the DOM (JS can change the DOM, so the browser *has* to find out if it intends to). This can add a full second to the page load time in some cases!
  • 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.
  • Try making several JS manifests that only load the libraries needed on particular parts of your site. You don’t have to put all your JavaScript dependencies into application.js (though if you do make other manifests, you need to tell Rails about them).

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.

Further Reading


Public and private clouds, using OpenStack and Ceph.
Like what you read? Give Sean Handley a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.