We recently went through a couple of optimisations on a Rails app that we’re building. The application is hosted on Heroku, but most of the points here will get you a long way even if you’re not using Heroku.
We wanted to compile a generic list of optimisation points that we found ourselves doing over and over, but if you feel that we’ve missed something, please let us know.
Amazon CloudFront CDN
Amazon CloudFront is probably the most straightforward CDN to implement. The idea is simple:
When someone visits a web page, the browser downloads the assets in parallel. To avoid overloading the server, it will only try to download a certain amount of assets from the same origin. Until all those assets are retrieved, it will not load any others.
Adding one (or multiple) CDNs to your app will allow you to serve more assets at the same time. Also, since those CDNs are optimized for content delivery (caching, multiple location redundancy and so on), it will probably be faster than you at serving those assets.
CloudFront is simple because you have almost nothing to do. The browser requests ‘image-a.png’ from your CloudFront distribution. If it has it cached, it will give it back, if not it will fetch it from your server first and then cache it.
Simple, easy and cheap.
How to setup CloudFront is also dead easy:
- Sign up to https://console.aws.amazon.com
- Create a new CloudFront distribution
- Setup your origin as your website domain
- Wait for it to propagate
Once done, add config.action_controller.asset_host = ENV["CLOUDFRONT_URL"]
to your environments/production.rb
You will now need to send this ENV to Heroku:
heroku config:add CLOUDFRONT_URL=https://dzsghkse.cloudfront.com
Once thing to note is that the CloudFront URL won’t match your domain. If you are not using HTTPS, you can create a CNAME and use your subdomain instead of the CloudFront garbage URL.
If you are using HTTPS, you still can but do it, but it will cost quite a lot of money. Currently around $600 per month. So unless you need your assets path to match your domain name, I would probably recommend you to keep the default CloudFront URL.
Paperclip, S3 and CloudFront
We are big fans of Paperclip and its simplicity (and almost of the thoughtbot‘s gems in general).
If you are using Heroku, you’re probably already serving your assets via Amazon S3. But even if this works, it’s probably not a good idea since S3, as great as it may be, is not optimised for asset delivery.
Thankfully, we can easily serve our S3 assets using CloudFront.
If you want to do this, I would recommend creating a new Behaviour in your CloudFront distribution and set it up as
Path Pattern: uploads/*
Origin: Your S3 bucket
Once done, you can tell Paperclip that you are now using CloudFront.
Create a config/initializer/paperclip.rb
containing
if Rails.env.production?
Paperclip::Attachment.default_options[:storage] = :s3
Paperclip::Attachment.default_options[:s3_credentials] = {
bucket: ENV["S3_BUCKET"],
access_key_id: ENV["S3_ACCESS_KEY_ID"],
secret_access_key: ENV["S3_SECRET_ACCESS_KEY"]
}
Paperclip::Attachment.default_options[:s3_protocol] = "https"
Paperclip::Attachment.default_options[:url] = ":s3_alias_url"
Paperclip::Attachment.default_options[:s3_host_alias] = ENV["CLOUDFRONT_URL"]
Paperclip::Attachment.default_options[:path] = "/uploads/:class/:attachment/:id_partition/:updated_at/:style/:filename"
end
Please note that we’ve changed the default Paperclip image path to match our CloudFront Behaviour, and to ensure a new file name if the asset is updated.
Now your Paperclip assets will be loading super fast, thanks to CloudFront.
If you already have some existing assets in Paperclip that need to be moved around or path to be changed, you can use this rake task that should do the job for you.
namespace :paperclip_assets do
desc "Migrate S3 images to new filenames"
task :migrate_images => :environment do
s3 = AWS::S3.new(access_key_id: ENV["S3_ACCESS_KEY_ID"], secret_access_key: ENV["S3_SECRET_ACCESS_KEY"])
bucket = s3.buckets[ENV["S3_BUCKET"]]
bucket.objects.each do |object|
next unless object.key =~ /\.(jpg|jpeg|png|gif)$/
path_parts = object.key.split("/")
# Assumes that old interpolation pattern was
# `/:class/:attachment/:id_partition/:style/:filename`
resource_id = path_parts[2..4].join.to_i
resource_class_name = path_parts[0].singularize.classify
attachment_name = path_parts[1].singularize
begin
resource_class = resource_class_name.constantize
resource = resource_class.find(resource_id)
new_path = resource.send(attachment_name).path
Rails.logger.info "Renaming: #{object.key} -> #{new_path}"
object.copy_to new_path, acl: :public_read
rescue => e
Rails.logger.error "Error renaming #{object.key}: #{e.inspect}"
end
end
end
end
A word about web fonts
When you move your site to CloudFront, if you’re hosting web fonts, you will eventually end up having CORS troubles. Depending on your need, this could be a real pain. So let’s have a look.
If you’re only using Font Awesome
If this is the case, and all your other fonts are loaded via Google Fonts or another service, I would recommend to not include Font Awesome in your CSS files, and just load it from the MAX CDN version. You can find the links here http://fortawesome.github.io/Font-Awesome/get-started/.
If you are hosting fonts
Given that your fonts are stored in app/assets/fonts, you will first need to install Rack CORS and follow their setup example.
You will also need to create a new CloudFront Behaviour, with /fonts/*
as a path pattern and your website as an origin.
In this one, you will have to Whitelist those four headers
- Access-Control-Allow-Headers
- Access-Control-Allow-Methods
- Access-Control-Allow-Origin
- Origin
Then select Forward query string.
Once your distribution is propagating, you should be sorted. But make sure to clear your cache before testing again.
Other small nice improvements
Those are simple things to do, but if were talking about serving a minimal page, every little helps.
Serve your CSS and JS as GZ
On Heroku, this would be as easy as adding use Rack::Deflater
into your config.ru
.
It might not be the most elegant solution, but it works nicely. Other solutions would include asset_sync gem or the rack-zippy gem.
Leverage browser caching
Since you’ve defined your Paperclip path to be changed when updating an asset, you will want to leverage browser caching to not serve the asset twice.
Just add this line to your config/environments/production.rb
config.static_cache_control = "public, max-age=31536000"
Optimise your assets
Your assets hold a lot of information, with most of it is useless to the browser. Optimising images will reduce your loading time for each of those assets.
Have a look at Image optim – it will automatically shrink public assets on compilation.
If you don’t want that to happen, you can install the gem and run it manually from the console.
Optimise perceived load time (Turbolink)
I’m a big fan of Turbolink. I know a lot of people dislike it, but I think it’s a great idea.
But since it only redraws your
element, navigating the app can sometimes feel strange. Like you click on a button and while the page loads nothing happen.
If you are using Turbolink, you can enable a nice progress loading bar, which will show at the top of your page.
Turbolinks.enableProgressBar(); // For the current Turbolink version (< 3.0)
Turbolinks.ProgressBar.enable(); // For Turbolink Edge (3.0+)
Then you can use the following CSS to control the progress bar style
html.turbolinks-progress-bar::before {
background-color: red !important;
height: 5px !important;
}
For simplicity, the Nprogress gem is a nice shortcut.
So, what’s next?
Well, implementing all this will certainly help your page load and improve your app grades in testing tools. While premature optimisation can be a bad idea, I think this list should be a minimum requirement for every app.
Depending on your needs, there could still be a little more work to do.
If you are using Heroku, remember that now Puma is the default web server.
You can also use New Relic or Skylight.io to catch your app’s slowest parts.
You can also speed up your views using Fragment Caching. The easier way is to follow the Rails doc and to configure Memcachier on your Heroku instance.
If you are using background jobs, I recommend you to have a look at HireFire.io. For $10/month, it will keep looking over your dynos, and increase them when your site needs it, and decrease them when not in use.
I hope this ‘guide’ has been useful, and if you have any more tips, please share with them with us!