Rails Asset Pipeline, CDNs and Serving Cross-Domain Fonts
Recently I was working on a project for a client. My task was straightforward: “Can you make my site faster?” There was only one right answer.
First Roadblock: Namespace Chaos
As it goes with Rails, the Asset Pipeline adheres to a few conventions, one of which is dumping all static assets into the same namespace. In order words, once compiled, a stylesheet and an image requested with
<%= stylesheet_link_tag "screen.css" %> and
<%= image_tag "icon.png" %> will be served from
/assets/icon.png, respectively, instead of from
/images/icon.png, as they would have been in previous versions of Rails.
This has the advantage of allowing you to reference static assets across libraries without needing to fuss with sub-folders. By default, the Asset Pipeline will bring into the asset search path anything under
I imagine for most this works fine. However, I see a few problems with it. First, there is potential for files to overlap across libraries. For example, it is conceivable that two libraries might have a stylesheet called
screen.css – which one would be imported? Second, it makes it difficult to organize and serve assets from multiple Content Delivery Networks, as your files are lumped together in one namespace. (Why multiple CDNs? More on that in a bit.) Finally, it pollutes the
public/assets/ directory with any uncompiled files that your third-party libraries may contain – even READMEs, tests and documentation. This became especially apparent when I dropped the Google Closure library into
vendor/assets/. When I ran
rake assets:precompile, I ended up with this:
$ ls public/static/ AUTHORS LICENSE README all_tests-2b7a9ce7ac70114656773b6a5eb47ffc.html all_tests.html application-9302184a59033b8f84b5c0c84d58beaf.js application-9302184a59033b8f84b5c0c84d58beaf.js.gz application-c57e646675665776d02ceda5774b69dc.css application-c57e646675665776d02ceda5774b69dc.css.gz application.css application.css.gz application.js application.js.gz arrow-left_20x14-102858d7051ca3cb338ec444dd1e41d5.png arrow-left_20x14.png beta_59x33-bbe810b7f16c710832057168e248cf9e.png beta_59x33.png bg-gradient-d1045408f33a832534950ec06b195fe3.jpg bg-gradient.jpg bullet_green-0995accbacf9bc3e11f805378cb085bc.png bullet_green.png bullet_orange-cf2e4090499d4dee019244b27993737b.png bullet_orange.png closure # ... and 86 more. You get the idea.
As you can see, any non-JS or CSS files found in the assets search paths are dumped into
public/static/, including the AUTHORS and README files from Google Closure. How was I supposed to maintain that? As I upload my static assets to a CDN like most, what would happen when I wanted to remove or upgrade a third-party library? Rather than find out, I decided to pass on dealing with that mess.
So I set out to fight the framework and namespace my static assets. There’s probably a really clever way to do this, but simply nesting my assets within sub-folders accomplished what I was hoping for. So,
vendor/assets/third_party/closure-library/, and so on.
When I compiled my static assets again,
public/assets/ looked like this:
Sanity had been restored.
Second Roadblock: Relative Image Paths in Third-Party Library Stylesheets
Moving assets into logical namespaces made managing assets easier in the long run, but it created a new problem: relative image paths in third-party libraries were broken. Fortunately, this only affected jQuery UI, as the Google Closure library used absolute paths for images (on Google’s CDN). After considering my options, I decided to search-and-replace relative path strings with the Rails asset helper in the jQuery UI stylesheet like so:
<%= asset_path 'jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png' %>. Again, there may be a more elegant solution, but this worked for my purposes.
The Asset Pipeline will by default compile two files:
app/assets/application.css. You can instruct it to precompile additional files like so:
config.assets.precompile += ['admin/admin.js', 'admin/admin.css', 'admin/editor.comp.js']
I had previously built a rich text editor with the Google Closure library for use in administrative back-ends. With
Since breaking up my Closure editor into multiple files wasn’t an option, I removed it from the precompile array and simply wrote a Capistrano deploy task to copy it to the assets directory and add an entry for it to the
manifest.yml file (so that the Asset Pipeline would recognize it) after the other assets were precompiled. It goes something like this:
With the my static assets organized into sane, modular namespaces, and with my rich text editor in place, I set about uploading everything to a CDN.
Fourth Roadblock: Firefox and Serving Fonts across Domains
It turns out that Firefox will not load a font file served from one domain accessed within another domain. So, a font hosted at cdn.therealretouch.com would be inaccessible from www.therealretouch.com when the website were viewed in Firefox.
The workaround for this is to use the
Access-Control-Allow-Origin header to specify the domain which would be accessing the resource (or, more flexibly,
*, to permit any domain). Unfortunately, the Amazon S3 API does not allow one to set the
Access-Control-Allow-Origin header. As a result, I needed another CDN solution that would allow setting this header when uploading fonts.
As the site was already being hosted with Rackspace, CloudFiles seemed a logical alternative. Thankfully CloudFiles has supported setting the
Access-Control-Allow-Origin header via their API since May 2011.
Since my client was already tied to Amazon S3 for order files, I decided to use CloudFiles for serving fonts only and to serve all other static content from Amazon CloudFront.
Here is the Rake task for uploading the compiled assets to Amazon S3 and Rackspace CloudFiles according to the hybrid CDN solution described above:
- Asset Pipeline. Sep. 2011. Rails Guides. 31 Jan. 2012 <http://guides.rubyonrails.org/asset_pipeline.html>.
- Understanding the Asset Pipeline. Aug. 2011. Railscasts. 31 Jan. 2012 <http://railscasts.com/episodes/279-understanding-the-asset-pipeline>.
- Upgrading to Rails 3.1. Sep 2011. Railscasts. 31 Jan. 2012 <http://railscasts.com/episodes/282-upgrading-to-rails-3-1>.