Throw away Sprockets, use UNIX!
… and check why 5600+ Rails engineers read also this
Throw away Sprockets, use UNIX!
The Sprockets gem is the standard way to combine asset files in Rails, but it
wasn’t very straightforward to use in stand-alone projects, like Single Page
Applications without backend, before the sprockets
command was added.
Few weeks ago I realized that Sprockets solve the problem that has been already solved, but in a different language and in different era of computing.
Later I wanted to check whether my idea would actually work and started hacking. You can see the results below.
The C Preprocessor
The designers of C language had to solve a similar problem, so they came up with a preprocessor that understands directives that allow concatenating multiple files into one. Additionally, it offers some macros and other stuff, but it isn’t really important in this application.
In most UNIX-like systems there exists a separate binary, called cpp
, that
can be used to invoke the preprocessor.
Its key feature here is that it can be used with any programming language, not necessarily C, C++ or Objective-C.
Let’s give it a try
Say I have two files, one called deep_thought.coffee
and the other one called
answer.coffee
. They’re listed below.
answer.coffee:
answer = 42
I'd like to use the `answer` in the other module of my application. It's really
simple with the `#import` directive, which includes the dependency only once.
deep_thought.coffee:
#import "answer.coffee"
console.log "The answer to the Ultimate Question is #{answer}"
Now let’s run the preprocessor and see what happens.
$ cpp -P deep_thought.coffee
answer = 42
console.log "The answer to the Ultimate Question is #{answer}"
Looks like it’s what we need. The only thing that’s left to do is to compile the file.
$ cpp -P deep_thought.coffee | coffee -s -p
(function() {
var answer;
answer = 42;
console.log("The answer to the Ultimate Question is " + answer);
}).call(this);
As you can see from the above, there is no magic and even old UNIX tools can get this work done properly.
Is it any good in practice?
The short answer is yes. To prove this I resurrected the hexagonal.js
implementation of TodoMVC and replaced
coffee-toaster
with a Makefile
listed below.
MAIN=src/todo_app.coffee
RELEASE_DIR=release
RELEASE_MAIN="$(RELEASE_DIR)/todo_app.js"
debug:
cpp $(MAIN) | coffee -s -p > $(RELEASE_MAIN)
release:
cpp $(MAIN) | coffee -s -p | uglifyjs > $(RELEASE_MAIN)
clean:
rm -f $(RELEASE_DIR)/*
.PHONY: debug release clean
That’s it. There are three targets defined: debug
, release
and clean
. The
default one is debug
. .PHONY
just means that there are no dependencies for
these targets and they should be executed every time.
You can see all the relevant changes in this
commit.
To compile it, just run make
from the command line and given you have
coffee
and cpp
command line utilities installed, it just works!
But is it faster?
To check it I modified the Makefile
to run Sprockets and performed simple
benchmark. I ran both versions in the clean environment 50 times and took an
average. The run time for Sprockets doesn’t include the time of running bundle
exec
. You can see the modifications on a separate
branch.
The cpp
took 0.23 seconds to compile the assets, while for Sprockets it was
1.57 seconds, which is almost seven times slower! Looks like it is doing a lot
more work than is needed to just compile few CoffeeScript files.
You can easily perform similar benchmark using the time
command if you don’t
believe the results.
When not to use it
You may have noticed some differences in the output file produced by the cpp
solution. There is only one wrapping anonymous function on the top level. This
is because it first concatenates all CoffeeScript files and then it compiles
one big file. Sprockets work the other way around - the files are compiled and
then they are concatenated. That allows mixing JavaScript and CoffeeScript
files.
Comments in CoffeeScript files don’t work either, because they are treated as directives for the preprocessor and are reported as errors. At Arkency we rarely use comments in the code - we believe that the code should be always readable without needing additional explanation in the comment. It isn’t an issue if you do the same.
The performance may be also a problem, even though the benchmarks show that
cpp
is clearly faster. However, when a single file is modified in the large
project, Sprockets recompile only that file, whereas in this solution all
imported files need to be recompiled.
Conclusion
The problem with Sprockets is that they are responsible for doing lot of tasks.
They have to manage the dependencies, run the compiler and then concatenate all
the resulting files. It is clearly, against the UNIX way. There should be one
component for each task. The make
command can be used to schedule the
compilation, compiler should only do the compilation, another tool should
create the dependency map and yet another one should put the resulting files
together using the compiled results and the dependency map. That’d be the UNIX
way to solve this problem!