Asset Pipeline Tips
After speaking to a few of the attendees at Red Dot Ruby Conf a few weeks ago in Singapore I promised them I’d document the techniques we use at Icelab to get the most from Rails asset pipeline. So here it is.
Gemfile
If the gem does not have any functionality that needs to be run on the server then we place it in the assets group. I’m repeatedly seeing some of the below gems in the main group of gems in some people’s projects. This was likely because of bugs in earlier versions of the asset pipeline which don’t exist now.
group :assets do
gem 'bootstrap-sass'
gem 'coffee-rails'
gem 'compass-rails'
gem 'jquery-rails'
gem 'sass-rails'
gem 'uglifier'
end
Just a note about compass-rails. If you’re upgrading this library from an older version you’ll be happy to know that a config file is no longer required for standard functionality.
Compiled Files
By default there is a compiled application.js
and an application.css
. We typically expand on this in application.rb
to something like:
config.assets.precompile += [
'admin.css',
'admin.js',
'fallback_jquery.js',
'modernizr.js'
]
We have the admin.js
and admin.css
files in app/assets/javascripts|stylesheets
next to their application counterparts. We place the fallback_jquery.js
and modernizr.js
in vendor/assets/javascripts
. The reason we have Modernizr in a separate file is so we can load it in the head of the document (although I’ve recently been informed that simply having Modernizr inline in the header results in slightly faster loading). Then in the footer we async load jQuery and the application.js or admin.js using Modernizr.load()
:
<!DOCTYPE html>
<html lang="en">
<head>
<%= stylesheet_link_tag 'application', :media => 'all' %>
<%= javascript_include_tag 'modernizr' %>
<body>
<%= yield %>
<script>
Modernizr.load([
{
load: ['timeout=3000!http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', '<%= javascript_path "application" %>'],
callback: function () {
if (!window.jQuery) {
Modernizr.load('<%= javascript_path "fallback_jquery" %>');
}
}
}
]);
</script>
</body>
</html>
As you can see we first attempt to load jQuery from the Google CDN and if that fails to load in 3 seconds it requests the local fallback version. Because we have the jquery-rails gem all that is required inside the vendor/assets/javascripts/fallback_jquery.js
file is:
//= require jquery
Why not just add jquery
to the list of precompiled files you ask? Because we simply wanted to be specific about it’s purpose. Which leads me onto the next point. Rename your vendor JS libs for consistency. Rather than just sticking jQuery.whizbang-2.213.js
in the vendor directory and doing //= require jQuery.whizbang-2.213
call the file jquery_whizbang.js
and paste in the non-minified version that includes the headers/version information. Having the vendor files named in a consistent fashion to the rest of your Rails application makes things a lot tidier.
Sprocket Requiring Files
We tend to stick to the simple //= require x
command and not require_tree
as it pays to be specific about load order as it will inevitably bite you one day. We leave the application JS/CoffeeScript files in the root of app/assets/javascripts
and place the admin stuff in a subdirectory which is then loaded in admin.js with //= require admin/file
. All the vendor JS libs get renamed for consistency and placed in vendor/assets/javascripts
. Some will be loaded in application.js
, some in admin.js
and some in both.
Do not sprocket require CSS/SASS files. Only use SASS @import
. Any vendor CSS we wish to use we place in vendor/assets/stylesheets
, rename for consistency and add a .css.scss
extension.
Asset Sync
In most situations these days we use the fantastic asset_sync
gem to upload our assets up to S3. It is all pretty straight forward and the gem is very well documented, just be sure to enable gzip compression as it will make a big difference to the S3 download sizes.
In the next section I’m going to detail how we precompile assets locally and if you choose to go this route then you can place the asset_sync gem in the Gemfile’s assets
group as it needn’t run on the server. Then in the asset_sync config initializer you need to wrapper everything in an if defined?(AssetSync)
end
.
Heroku
This seems to be the biggest grievance amongst the people I have talked to about the Rails asset pipeline. When pushing to Heroku either the precompilation of assets fails completely or it simply drives them insane with how long it takes to deploy. Especially so if they are only wanting to deploy a quick code fix as Matt alludes to in his tweet. The technique we have come up with, and we acknowledge it isn’t perfect, is to force git add the pubic/assets/manifest.yml
while still git ignoring the entire public/assets
directory, and then running rake assets:precompile
locally which uploads to S3 with the asset_sync gem. Next you will be required to commit any changes to the manifest.yml
and then you are good for a deploy. When pushing the code to Heroku it will detect the presence of the manifest.yml
file and will now not attempt to precompile the assets.
From our experience this workflow is much more reliable. It is however a bit of a pain remembering to precompile the assets and commit the changed manifest.yml
when you have been working on them. It is a huge win though for speeding up quickfix code deploys, so we think it is worth it.
These are the notes we throw into the Readme of every project where we use this technique:
Asset Compiling
Assets are precompiled locally and automatically uploaded to 3S with the asset_sync
gem before deploying with the command:
bundle exec rake assets:precompile
For this to work you need in place:
- The
config/config.yml
file with the AWS details (seeconfig/config.example.yml
) - A production database connection adapter (see
config/database.example.yml
)
Following compilation the public/assets/manifest.yml
will have changes in git which need committing before deployment. The compiled files will be in public/assets but they are git ignored.
Note: when developing locally if there are files in public/assets
they will get loaded so you won’t see your changes (not good). To fix this:
bundle exec rake assets:clean && git checkout public/assets/manifest.yml
When deploying/pushing to Heroku it detects the presence of the manifest.yml
file and does not attempt to compile the assets itself. The asset_host
has been set to the S3 domain in production.rb
.
Feel free to copy it into yours.