Optimize Rails App Performance With Rails + Amazon CloudFront

(Image credit: PEXELS)

This is a technical guide that is talking about optimizing performance technique of delivery static assets of Rails app with using Amazon CloudFront CDN on the assets pipeline.

As Web usability, it is not just about the User Interface: a large component of usability is speed. On the web, that translates to page load time performance. While focusing on application code is important, there are many quick win improvements that can be made to the User Interface to dramatically speed up page loads.

Using a Content Delivery Network (CDN) optimises the delivery of static assets on web site. This allows us to offload all requests for these static assets off of our web dynos, which in turn will free those dynos to handle more requests for dynamic content.

I’m surprised this technique isn’t in more widespread use as it reliably makes for snappier user experiences, especially users with a fresh cache.

This guide assumes you’re using the Rails asset pipeline to package and serve your assets. If you’re not using the asset pipeline, whatever solution you use to achieve the same result will need to fingerprint asset filenames with some sort of unique identifier for it to work smoothly with Cloudfront or a CDN like it.

Prerequisites

  • To use AWS CloudFront, you will need an Amazon AWS account (even free tier).
  • Ruby on Rails application should be able to deploy to the Internet.
  • Should know the hostname or at least a public IP address for where the application is hosted.
  • I’m implementing and testing on Rails 5 with Nginx serves static files.

Amazon S3 — Use Case

Many developers make use of only Amazon S3 service for serving static assets that have been uploaded previously, either manually or by some form of build process. Even it is mentioned in S3 use cases, S3 just serves static file on its server. I have only experienced bad download times from it (especially, S3 buckets allocated to region that different to original requests sent.) so that I’m not recommended this works, as S3 was designed as a file storage service and not for optimal delivery of files under load. Therefore, serving static assets from S3 is not best practice.

Amazon CloudFront (Amazon CDN)

How AWS CloudFront works

CloudFront is an offering from Amazon Web Service that speed up distribution of static and dynamic web content, such as .html, .css, .php, and image files, to end-users. CloudFront delivers contents through a worldwide network of data centers called edge locations. [Amazon CloudFront Developer Guide]

Edge cache locations are groups of servers distributed around the world optimised for the high throughput serving of small static files. Using latency based DNS resolution, Amazon are able to route requests to particular assets to the closest edge cache location to the end user reducing latency, and improving speed.

CloudFront works from what is called a distribution. A distribution is the direct equivalent of an S3 bucket. Each distribution is assigned a domain name that is used to access distributions assets.

A distribution draws its contents from an origin. The origin is where CloudFront will find files for distribution from CloudFront edge locations all over the world if it not have a copy within the distribution. Once a file has been requested from the origin CloudFront will cache that asset and return it directly to the end user.

Source: Amazon CloudFront Developer Guides

To work with CloudFront you first need the Ruby on Rails application should be able to alive on the cloud then need to set up a distribution. CloudFront sends your distribution’s configuration (not your content yet) to all of its edge locations.

CloudFront configuration flows:

Source: Amazon CloudFront Developer Guides

As develop your website or application, you use the domain name that CloudFront provides for your URLs. For example, if CloudFront returns the domain name looks like the following:

d111111abcdef8.cloudfront.net

The URL for logo.jpg in the root directory on an HTTP server will be

https://d111111abcdef8.cloudfront.net/logo.jpg

Then when you make the above request to a CloudFront distribution, the first time it receives that request, the distribution will forward that request to:

https://aromajoin.com/logo.jpg

And cache whatever it finds there for future requests.

Creating a new distribution

Once logged into the AWS management interface, you can go to the CloudFront control panel and select ‘Create distribution’. When prompted for the delivery method, select ‘Web’.

Then, the default settings should be fine for your application. If you want to set up a custom domain for using the distribution (for example, cdn.aromajoin.com) you can enter it in the field labeled ‘Alternate Domain Names (CNAMEs)’ but you can always add this later if you prefer.

You must enter domain under ‘Origin Domain Name’.

Once created, Amazon will spend a short time setting your distribution up ready for use. This usually takes around five to ten minutes to complete (Up to 15 minutes for me.). You are able to track the status of this via the indicator in the CloudFront user interface.

Testing configuration

Before moving onto the next stage, I would recommended testing manually to see that the distribution is working correctly. To do this, take the URL of one of your assets and attempt to retrieve it using the distribution domain.

For example, if we have a css file at the following URL:

https://aromajoin.com/assets/application-99b3c0f37d3bfc5fb428f3312698aaa9.css

And you’ve configured the distribution to have the custom origin of www.aromajoin.com, then you should be able to get that file at the following URL:

https://d111111abcdef8.cloudfront.net/assets/application-99b3c0f37d3bfc5fb428f3312698aaa9.css

Making CloudFront works with Rails assets pipeline

The Rails asset pipeline has the handy feature of compiling the assets and then appending a hash of the contents to filename. This means that if the contents of the file change, then the filename changes too. This is perfect for working with an edge cache like CloudFront, as it never needs to expire objects.

Configuring the Ruby on Rails to use CloudFront as the asset host for static assets is truly a one line change.

In config/environments/production.rb:

config.action_controller.asset_host = ENV['CLOUDFRONT_ENDPOINT']

Where ENV[‘CLOUDFRONT_ENDPOINT’] is a environment variable pair:

  • Key: CLOUDFRONT_ENDPOINT
  • Value: The URL of your CloudFront endpoint, available in the CloudFront console. For e.g., “d111111abcdef8.cloudfront.net”
  • You do not need to “Protect” this value.

This has the effect of making all the links you send use the configured asset host instead of that of your own website.

For example, where the main application stylesheet may have been served like this in your HTML head:

<link href="/assets/application-99b3c0f37d3bfc5fb428f3312698aaa9.css rel="stylesheet">

The href will now have the configured asset host prepended:

<link href="https://d111111abcdef8.cloudfront.net/assets/application-99b3c0f37d3bfc5fb428f3312698aaa9.css" rel="stylesheet">

CORS Issue

If you are loading all the assets on your site via a CDN then the browser may give warnings or errors about using resources from different origins. Firefox, Chrome in particular may complain when serving assets:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource .

There is a mechanism that allows you to communicate to the browser which assets can be loaded at specific origins safely. This is known as cross origin resource sharing or CORS. It works by setting specific headers when serving the assets. CloudFront caches headers when it caches your assets so if your app serves assets with the appropriate CORS headers then CloudFront will cache them and serve them.

As far as I have experienced, there are 3 solutions that depend on your server situation:

  • You can set headers using ActionDispatch::Static in Rails 5 or via rack-cors gem. It works if you are using Rails stack for serving static files itself.
  • If you have configured Nginx proxies which will serve static assets, take a look at these references here and add config CORS custom headers correctly.
  • As if you can change using static assets from your origin server to other CDN services, that will resolve this issue without additional configuration. For example: with font assets, you should try to use Google Fonts service instead of store on your own web server.