Rails Asset Pipeline Tip: Keep It Shallow
Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.
The Rails Asset Pipeline was envisioned before JS module solutions were robust and common. Consequently, it comes with it’s own way to manage dependencies. In the application.js
of any non-trivial app, you will find comments specifying which files to include into this file during deployment.
//= require jquery
//= require frameworkjs
//= require app
And often, an app.js
that contains further dependencies.
//= require_self
//= require storage
//= require_tree ./models
//= require_tree ./controllers
//= require_tree ./views
window.App = Framework.createApplication();
When any file changes, itself and all files dependent on it are marked invalid. If controllers/session.js
is changed, three files would become invalid:
controllers/session.js
app.js
application.js
After marking these three files as invalid, Rails will try to recompile them before responding to a request. Even if the results of an asset compilation are the same (the JavaScript returned in app.js
does not change if controllers/session.js
changes), Rails will not respond with a 304. It will rebuild all invalid dependencies, even if you are not asking for those dependencies.
In development mode, Rails explodes dependencies into multiple <script>
tags. Each file is linked to and requested separately. app.js
is a dependency of applications.js
, but these files are fetched independently in development mode.
This behavior is a little over-complex, but understandable. With just JavaScript, the speed of resolving asset dependencies isn’t so bad.
But most of us building complex JS apps aren’t using JavaScript, are we? And we make the change-source-code-reload-a-page cycle hundreds of times a day.
CoffeeScript
Here is an example tree of JavaScript files being compiled into application.js
. First a fresh request, then a second one cached.
Started GET "/assets/application.js" for 127.0.0.1 at 2013-06-01 09:54:45 -0400
Compiled controllers/session.js (0ms) (pid 47538)
Compiled views/session.js (43ms) (pid 47538)
Compiled app.js (50ms) (pid 47538)
Compiled application.js (4ms) (pid 47538)
Served asset /application.js - 200 OK (77ms)
Started GET "/assets/application.js" for 127.0.0.1 at 2013-06-01 09:54:50 -0400
Served asset /application.js - 200 OK (0ms)
77ms is fine. But the same tree with CoffeeScript files:
Started GET "/assets/application.js" for 127.0.0.1 at 2013-06-01 09:58:10 -0400
Compiled controllers/session.js (674ms) (pid 47591)
Compiled app.js (604ms) (pid 47591)
Compiled application.js (1586ms) (pid 47591)
Served asset /application.js - 200 OK (1596ms)
Started GET "/assets/application.js" for 127.0.0.1 at 2013-06-01 09:58:15 -0400
Served asset /application.js - 200 OK (0ms)
1.6 seconds! In this example I’m fetching application.js
, but keep in mind that the asset pipeline will recompile all the dependent files when any dependency changes. Even if their generated output will be the same. app.js.coffee
and application.js.coffee
may not have changed, but they will be recompiled nonetheless.
The simplest way to improve performance is to keep your asset dependencies short. There are two ways to do this.
Shallow Dependencies
Manage all your dependencies in a single file. By keeping the hierarchy of dependencies shallow, you decrease the number of files that need to be built after a given change. application.js
should look like:
// Libraries
//
//= require jquery
//= require frameworkjs
// Application files
//
//= require ./app
//= require ./storage
//= require_tree ./models
//= require_tree ./controllers
//= require_tree ./views
And no other files should contain dependencies. Even if your project uses CoffeeScript, keep application.js
as a JavaScript file to avoid running the expensive CoffeeScript compilation process unnecessarily.
In the development environment, Rails’ default behavior is to explode dependencies into multiple <script>
tags. On larger projects, this overwhelms the browser or crashes the Rails server. If you are disabling this behavior and serving a single file in development, using a shallower dependency tree should improve compilation time significantly.
Modules
Alternatively, use a JavaScipt module system. Unfortunately, there isn’t a maintained Asset Pipeline integration of my favored module system, minispade. There is a rails-minispade gem, but it seems to be defunct. https://github.com/jwhitley/requirejs-rails isn’t quite seamless to drop in, making it somewhat difficult to recommend. The es6-module-transpiler is new and interesting, but I see no immediate tools for using it with the asset pipeline.
Given a module system, you should be able to concatenate dependencies in an arbitrary order. This gives the asset pipeline very little work to do. application.js
would look like this:
// Require everything in no particular order
//
//= require_tree .
However, there isn’t a non-trivial way to do this with the Rails Asset Pipeline today. A better option is to use rake-pipeline and rake-pipeline-web-filters, which are designed with this kind of asset compilation in mind.
Britt Ballard published an excellent post explaining and demonstrating the benefits of module systems beyond better performing asset compilation speeds. Tom Dale’s post AMD is Not the Answer is also good reading on modules.