Blog

12 Tips for the Rails Asset Pipeline

By Mikel Lindsaar, 22 Oct 2013

Face it, you are a top developer, one of the best at your game, you eat lambda's for breakfast, refactor whole apps as an afternoon snack and bad code just trembles when you approach. You are at the top of your game and nothing stands in between you and the code you desire. Yet, the Rails asset pipeline still trips you up, not once, not twice, but just about every time you try and use it.

Here's a list of things that I have learnt that aren't necessarily obvious, that can help you tame this wild and savage beast.

1. Two options, precompile OR include, not both

No CSS or JS files will be available to your app through the asset pipeline unless they are included in other files OR listed in the config.precompile directive (see #2 and #3 below).

Only application.css and application.js are available by default of all the CSS and JS files.

For example, you make a file in the assets folder called:

app/assets/stylesheets/site.css.scss

And you want to include this in your page. You have two options, either include it as one of the CSS files to be loaded up in application.css with the following:

// = require_self
// = require 'site'

And then it will be inside the compiled public/assets/application.css which is included with:

stylesheet_link_tag "application"

OR you can add it to the precompile list like so:

config.assets.precompile += %w( site.css )

and then reference it with:

stylesheet_link_tag "site"

The reason this happens is that when you run rake assets:precompile Rails looks through your assets folder and copies over everything that is not Javascript or CSS into public/assets. It then creates application.js by reading app\assets\javascripts\application.js, and application.css by reading app\assets\stylesheets\application.css, loading up all the "require" files it finds in there, and does not look at any other file unless you explicitly tell it to.

2. File extensions matter in the precompile directive

Another common mistake is forgetting to put the file extension on the precompile directive, for example:

config.assets.precompile += %w( site.css )

Will do what you expect, make public/assets/site.css available on compile, but

config.assets.precompile += %w( site )

Will fail during the compilation phase as the site file doesn't exist.

3. The asset pipeline is not quite your assets folder

Every file that is not a Javascript file or CSS file that is in the app/assets folder will be copied by Rails into the public/assets folder when you compile your assets.

This is a source of much confusion, because note that this does NOT apply to Javascript or CSS.

So if you want to add some web fonts, you could make an app/assets/fonts/ folder and put your fonts in there, these will then be copied to public/assets/fonts folder when you compile your assets. Note that your app/assets/stylesheets/fonts.css.scss file that references those fonts will NOT be copied over unless you either added it to the config.assets.precompile directive or required it from your application.css file as per #1 above.

4. Get '/assets' does not resolve to '/app/assets' for stylesheets and javascript

This is a common mistake I see, people put a stylesheet into the app/assets/stylesheets folder or a javascript file into app/assets/javascripts folder and then reference it in development from their layout file and it all works, but when they deploy to production, everything blows up.

This is because, as per #1 above, these files won't get copied over into the public/assets folder unless you explicitly tell them to. Common gotcha. There is no hard and fast rule here, except having a staging environment and following #5 below.

5. Don't fall back in staging or production

First of all, you need to understand what the directive config.assets.compile does.

If it is set to "true" (which it is by default in development) then Rails will try to find a Javascript or CSS file by first looking in the public/assets directory and if it can't find it, will hunt through your app/assets folder looking for the file. If it finds it in app/assets it will go ahead and compile on the fly and then serve this asset up.

The problem with this is that you don't notice it happening in development, then you commit everything and push to production and BOOM, everything is broken with 500 errors because production has config.assets.compile set to "false".

Why don't you just have this set to "true" in every environment? Well, because it is sloooooow. And you don't want slow in production.

One of the first things I do on an app is make sure my config/environments/production.rb and config/environments/staging.rb have the following set:

# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = false

This prevents the app from "falling back" and trying to load the file directly instead of using the asset pipeline. This might mean that you get failures, but it is better to fail fast than to find our later that something has been broken for months.

It also means you know if your assets and site works in staging, then it is going to work in production, this is a good thing.

Note, Heroku for quite a while will override this setting to true automatically if your asset compilation fails, I believe this creates more confusion than benefit and I believe they deprecated this as of the 20th of September 2013.

6. Deploying with precompile can suck

If you use the I18n gem or need to load up your environment with asset precompilation it means your deploy process will take longer. This can suck and lead to various problems, missing databases and all sorts.

One place where this can be a pain is on Heroku. During the app deploy process, the database has not been configured which means that loading the environment during assets precompilation just doesn't work.

Two possible solutions to this problem are to either precompile locally and commit the assets folder before you push, or use Heroku's labs feature which may or may not exist in the future.

To precompile locally, you need to do the following:

  • Setup a dummy production database on your local machine
  • Create a production entry in database.yml
  • Add in any other dummy settings to load a production environment
  • Run RAILS_ENV=production rake assets:clean assets:precompile
  • Wait
  • Wait some more
  • Commit the results in the public/assets folder
  • Deploy this to Heroku

To use the Heroku labs precompile feature, follow their guide page

Note: I don't mean to harp on about Heroku (I've mentioned it twice so far) but their platform means you need to change the way you operate a little bit, other platforms might also have problems as well :)

7. Semi colons matter

Another thing that can catch you out is that various minify tools for Javascript might miss required semi colons, we had an interesting problem in one application where a javascript file didn't have a trailing semi colon on the last function block. When the files were all munged together to form application.js we got Javascript syntax errors because we were missing a semi colon.

So just put them in there, they are good for you.

8. Clean up after yourself

If you are trying to debug an asset pipeline problem, try running rake assets:clean assets:precompile first. This removes all old assets and rebuilds them from scratch, meaning you get a clean new set to play with.

9. It's all relative, mostly

In the Stylesheets and Javascript files you can use require, require_tree and require_directory to reference other files to include. The thing to remember is that all of these reference different things.

require will look for any file that has the same name that is in in your assets load path. Don't know what your assets load path is? Well, fire up Rails console in the environment of your choice and run: Rails.application.config.assets.paths.

require_tree will look for any relative directory path starting from current directory (usually app/assets folder) and then load every file under it recursively, so: require_tree './my/directory' in the application.js file will attempt to load every file in the app/assets/javascripts/my/directory as well as every file in every directory nested within app/assets/javascripts/my/directory. Also, something like require_tree '../../../something' will do crazy things, just don't do that :) If you find yourself putting lots of require_tree declarations, consider instead adding those directories to the assets.paths config settings like so:

config.assets.paths << File.join(Rails.root, '/my/special/path')

require_directory will look only for a directory name starting at the current directory. So require_directory "." will load every file in the current directory in a non recursive manner. require_directory 'special' will load every file in the app/assets/special directory, but will not look at or load any files in the directories nested within the special directory.

Obviously this will assume a file extension that matches the type of file you are loading - "js" for javascript and "css" for stylesheets.

10. Don't require self

A common thing I see in application.js and application.css is the require_self directive. This special command tells the rails app to include any Javascript or CSS that is in the application.js or application.css file, which is bad form. These files should function as an index of the assets below, and should be kept free of clutter. Just move that stuff into another file and require it using the require commands listed in #9 above.

11. Use the helpers!

Another gotcha is that you should no longer do things like:

<img src="assets/my-image.jpg" />

Because doing this will serve up the image but not append the special cache-busting timestamp that Rails appends as part of it's assets precompile. Instead use the image_url and image_path helpers to make sure you get the right URLs.

12. Read!

The last tip is to go and read the excellent Rails Asset Pipeline Guide which does contain all of the above, in parts :)

Footnote

If you find anything in the above that is incorrect or have a suggested tip to include, please email me and I'll get it fixed up :)

blog comments powered by Disqus