Optimizing website performance on Ruby on Rails

October 19, 2017Engineering

Speeding up your marketing website has a positive impact on customer experience and search ranking. That’s exactly what I did this week and in this post I’ll show you how!

This week I spent some time optimizing our marketing website, aiming for a Google PageSpeed rank of 100. Why? Well, a fast website performs better in search results, especially on mobile. Performance is also a critical part of a great user experience—a fast website increases trust and confidence, and reduces frustration. Potential customers are more likely to keep clicking around and sign up if the website is snappy.

Our marketing site runs Ruby on Rails on Heroku. This is a holdover from Dovetail 1 which was built entirely on Rails. We’ve since moved our app to React, but the marketing site is still on Rails and will be the foreseeable future. This post will detail specific Ruby on Rails performance optimizations, however the general concepts apply to any website setup.

The first thing to do is to take a baseline measurement of your production environment. Head over to PageSpeed Insights and enter your URL. After each optimization, check again to make sure you’re improving. We’re currently sitting on a PageSpeed score of 96/100. (I’ll explain why we’re not quite 100 at the end.)

Minify HTML, CSS, JS, and image assets

Minification refers to the process of removing unnecessary or redundant data without affecting how the resource is processed by the browser — e.g. code comments and formatting, removing unused code, using shorter variable and function names, and so on.

The easiest thing to do is to ensure everything is concatenated and minified. The Rails asset pipeline concatenates and minifies JavaScript & CSS by default when it runs in the production environment. It also minifies HTML. If you’re not running Rails, there are plenty of libraries that will concatenate and minify code for you in any language.

The next step is to compress images. I run all our images through ImageOptim at 80% quality with lossy minification enabled. We’re mostly serving SVGs or PNGs @2x where the drop in quality from lossy minification is not noticeable and the file size gains are worth it.

Serve compressed assets

All modern browsers support and automatically negotiate gzip compression for all HTTP requests. Enabling gzip compression can reduce the size of the transferred response by up to 90%, which can significantly reduce the amount of time to download the resource, reduce data usage for the client, and improve the time to first render of your pages.

Rails will serve gzipped HTML, JS, and CSS by default but it won’t serve gzipped SVGs even though it generates .svg.gz files. The solution for now is an unfortunate monkey patch in an initializer that tells Action::Dispatch to serve gzipped SVGs along with other files.

Defer JavaScript

Before the browser can render a page it has to build the DOM tree by parsing the HTML markup. During this process, whenever the parser encounters a script it has to stop and execute it before it can continue parsing the HTML. In the case of an external script the parser is also forced to wait for the resource to download, which may incur one or more network roundtrips and delay the time to first render of the page.

To ensure the browser isn’t waiting for non-critical JavaScript, we can defer it to be executed later with a quick change.

However, if you’re interacting with the DOM, you’ll need to wrap your JavaScript in a listener to wait for it to be fully loaded. Here’s an example of us doing that for Mixpanel.

Leverage browser caching

Fetching resources over the network is both slow and expensive: the download may require multiple roundtrips between the client and server, which delays processing and may block rendering of page content, and also incurs data costs for the visitor. All server responses should specify a caching policy to help the client determine if and when it can reuse a previously fetched response.

PageSpeed complained about some assets having short cache expiry times and some not having expiry times at all. To fix this in Rails on Heroku, we need to add the heroku-deflater gem to our Gemfile which lets us set default headers for assets via a line in our production.rb config file.

Reduce network requests

Our website doesn’t have a lot of CSS, mostly because I removed Bootstrap entirely (see below). Since our total CSS comes in around 5kb, it’s perfect to inline. This saves the browser from making another network request to fetch the CSS externally.

The approach I used was to create a helper which reads the file and inlines it as a