dynamic_assets

DynamicAssets allows a Rails 3.0 app to serve its JavaScript and CSS assets dynamically instead of statically, which makes all kinds of things possible. Out of the box it can (optionally):

  • Combine all CSS files into one for faster downloading.

  • Combine all JavaScript files into one.

  • Minify assets to make them smaller.

  • Run your CSS or JS assets through ERB so they can reference helpers, like views.

  • Run your CSS assets through a Sass pre-processor (sass or scss).

  • Run them through ERB then Sass, which can be useful to allow your app to set some Sass variables.

  • Combine, minify, and pre-process in memory instead of on disk, to accommodate read-only filesystems (e.g. Heroku).

  • Set Cache-Control and Expires headers for far-future expiration, allowing browsers and front-end caches like Varnish or Rack::Cache to hold assets for a long time.

  • Group assets, much like Scott Becker’s venerable asset_packager. (Example: You may want a set of stylesheets for your main interface, and another set for your admin interface, maybe with some overlap. With DynamicAssets, your normal users won’t pay the penalty of downloading your admin styles.)

  • Invalidate caches and CDNs by inserting a SHA1 signature into the asset URL path instead of using the Rails scheme of appending a URL timestamp. Some asset servers (notably Amazon CloudFront) will drop parameters from the URL, so cache-busting requires path-changing, and since assets are often moved from machine to machine, modification times can be unreliable.

  • Honor Rails’ scheme for asset hosts.

It seems that Rails 3.1 will offer many of these features off-the-shelf, which is cool. DynamicAssets allows you to get some of those features today in 3.0, but it wasn’t intended as a back port or a stopgap. It just happens that serving assets dynamically is useful enough that multiple people have thought of implementing it. (See also: Shoebox and Sprockets)

How To

  1. Add this to your Gemfile:

    gem "dynamic_assets"
    

    And if you’re planning to use Sass or SCSS, add this, too:

    gem "haml", "~> 3.0"
    
  2. Put your CSS files in app/assets/stylesheets and your JS files in app/assets/javascripts. Each filename’s extension triggers an optional pre-processor:

    .css          = raw CSS
    .js           = raw JS
    .css.erb      = process with ERB
    .js.erb       = process with ERB
    .sass         = process with Sass and assume sass syntax
    .scss         = process with Sass and assume scss syntax
    .sass.erb     = process with ERB then Sass (sass syntax)
    .scss.erb     = process with ERB then Sass (scss syntax)
    

    (Note that each file can be processed differently. You could stick one toe into the Sass world by renaming one of your .css files to .scss.)

  3. Create a config/assets.yml file that looks something like this:

    ---
    
    config:
      production, staging, test:
        combine_asset_groups: true
        minify: true
        cache: true
      development:
        combine_asset_groups: true
        minify: false
        cache: false
    
    javascripts:
    - base:
      - foo
      - bar
      - baz
      - third-party/widget
    - admin
      - foo
      - qux
      - quux
    
    stylesheets:
    - app:
      - reset
      - application
      - sidebar
    - admin
      - reset
      - application
      - admin
    

    The assets.yml file sets some config values and then lists your assets. Don’t be shy about listing your assets; it’s a good way to get noticed. This sample config file says that in production, foo.js, bar.js, baz.js, and widget.js (which is in app/assets/javascripts/third-party) should be combined into one file, minified, and served by your app as /assets/javascripts/base.js in such a way that it’d be cached. In development, those files would be combined but not minified or cached.

  4. In your layout, replace your usual CSS and JS references with

    <%= stylesheet_asset_tag :app %>
    

    wherever you want your stylesheet tags to appear and

    <%= javascript_asset_tag :base %>
    

    wherever you want your script tags to appear. The symbol argument to each helper is the name of the group you defined in assets.yml. Each helper will generate one tag if the assets are combined or multiple tags if they’re not, much like the cowboy in Mulholland Dr.. For example, if your assets.yml says to combine asset groups, stylesheet_asset_tag(:base) will insert this one tag into your page:

    <link href="/assets/stylesheets/1302901941/base_v2.css" media="screen" rel="stylesheet" type="text/css" />
    

    but if your assets.yml says not to combine them, stylesheet_asset_tag(:base) will insert one tag per CSS file:

    <link href="/assets/stylesheets/1302901941/reset.css" media="screen" rel="stylesheet" type="text/css" />
    <link href="/assets/stylesheets/1302902000/application.css" media="screen" rel="stylesheet" type="text/css" />
    <link href="/assets/stylesheets/1302901403/sidebar.css" media="screen" rel="stylesheet" type="text/css" />
    

Static Image URLs Embedded in CSS

Suppose you install a JavaScript plugin that comes with a stylesheet and some images. The stylesheet, thing.css, may reference one of its images with a relative URL, like this:

div.thing {
    background: url(fancy_background.png);
}

Browsers will find the image by looking in the same URL path as the stylesheet, so in a typical Rails environment, you might add these files to your app as public/stylesheets/thing.css and public/stylesheets/fancy_background.png.

With DynamicAssets, you’ll put them here instead:

app/assets/stylesheets/thing.css
public/stylesheets/thing/fancy_background.png

and the processor will make sure the embedded URL is turned into a fully qualified one that will allow the browser to find /stylesheets/thing/fancy_background.png.

Or, if you prefer to keep your third-party images cleanly separated from your own, you could put them in a subdirectory:

app/assets/stylesheets/third-party/thing.css
public/stylesheets/third-party/thing/fancy_background.png

DynamicAssets expects the images that belong to an asset to be in a parallel directory structure under public/stylesheets.

CSS Media Types

If you have different CSS files for printing than for the screen, create separate groups in your assets.yml. Then include both groups in your layout:

<%= stylesheet_asset_tag :app %>
<%= stylesheet_asset_tag :app_printing, :media => "print" %>

If no :media attribute is supplied, stylesheet_asset_tag will use “screen”.

Performance

In production, assets can typically be cached aggressively. dynamic_assets adds a signature to the asset path, and since it’s based on the last-modified time of the underlying assets, clients will be forced to reload to reload assets when they change. With caching, dynamic assets are quite speedy because you generate them rarely.

But during development they can be annoying if you set your environment to maximum slowness. The sweet spot for my dev configuration is to combine assets but not to minify or cache them (as shown in the assets.yml above). Here’s why:

Set assets not to be minified in development, usually

I usually leave minification off in development, because it can add some overhead to each asset request, and it makes the assets difficult to read if you need to debug them (like with Firebug). In production, caching virtually eliminates the overhead on all requests after the first one, but you typically won’t cache your assets in development, unless you’re some sort of nut.

Set assets to be combined, even in development and especially in test

By default, in development, Rails reloads all classes with each new request. (This is controlled by the cache_classes config parameter.) So if you’re not combining all of your assets, a single page load will result in an additional request to your app for each asset, which may result in a dozen requests to your dev server for each page, and each of those dozen requests will reload all of your classes. Combining assets in dev reduces the number of requests, shrinking your page load time. And unlike minification, using combined assets in dev is usually not a problem, even when you’re debugging them, because the concatenated files are still quite readable. The one exception is that combined files can make it difficult to find out which asset file includes the problem you’re hunting down.

Note that one advantage of using DynamicAssets instead of a deploy-time task is that you can get more exposure to your processed JavaScript. Concatenation and minification can sometimes uncover bugs in your scripts. (Example: a forgotten semicolon may be forgiven by a browser if it’s at the end of a script file but it may cause problems if it’s immediately followed by more code, tacked on from another file.)

Call me silly, but I prefer to find out about that kind of error before I deploy my app, and with DynamicAssets I can easily set my test environment to combine and minify, so full-stack testing will expose the problem.

To eliminate the biggest bottleneck, turn off class caching in development (but…)

If (big if) you don’t mind restarting your server every time you change a bit of Ruby code, you could edit your config/environments/development.rb to do this

config.cache_classes = true

which would eliminate the biggest chunk of overhead in dev, class-reloading on every request. And if you’re doing that, you might as well set your asset files to be cached, too, in your assets.yml.

Copyright © 2011 Robert Davis. See MIT-LICENSE for further details.