Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Allow your Rails 3 app to package and process your CSS and JS assets on the fly.
Ruby JavaScript
Branch: master
Pull request Compare This branch is 6 commits ahead, 33 commits behind davisre:master.

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
app
config
lib
spec
.autotest
.gitignore
.rspec
Gemfile
MIT-LICENSE
README.rdoc
Rakefile
VERSION
dynamic_assets.gemspec
init.rb
install.rb
uninstall.rb

README.rdoc

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, like views.

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

  • Run them through ERB then Sass, what the heck. (Actually, this can be useful, like 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 to hold assets for a long time.

  • Allow assets to be grouped, 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.)

  • Allow CSS assets to refer to static images through relative URLs. That is, it doesn't break URLs embedded in CSS.

  • 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"
  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" />

Variables for ERB

By default, assets are served by a small controller whose routes are added to your app automatically when the gem is loaded, but you can easily create your own controller if you prefer. One reason to do this would be to inject variables into an asset via ERB, like this:

class AssetsController < ApplicationController
    include DynamicAssets::Controller

    def show_stylesheet
      @background_color = '#FFE'
      render_asset :stylesheets, params[:name], "text/css"
    end
end

Now in app/assets/stylesheets/application.css.erb you could do this:

body {
    background-color: <%= @background_color %>;
}

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 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

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

Something went wrong with that request. Please try again.