UglifyJS v2 news
Update (Sep 18): I posted more news. The compressor beats UglifyJS v1 now on jQuery and on other libraries I tried. Also, the jQuery test suite is fully covered after minification.
First, a big THANK YOU! to everybody who donated so far! I was silent since I started the funding campaign, but my motto is “work, not talk”. I've been hard at work actually and I'm happy to announce that UglifyJS v2 has got support for generating source maps, a pretty exhaustive compressor (it beats v1 in some of my tests) and a minimal CLI utility.
I humbly remind you that the pledgie is still open and still far
from the target. If you like what I'm doing, please consider
contributing a few bucks!
UPDATE (Sep 07): Thanks to significant contributions from the jQuery Foundation and the Dojo Foundation, and from many individuals who heard the buzz, we are a lot closer to the target! Thank you people!
Update (Sep 11): Telerik matched the donations from jQuery and Dojo, providing more than enough to complete the campaign target! Thank you guys! I've been working on it, I'll post some updates and new code soon! What follows is old news.
Next, some information about the current state of affairs.
Command-line tool
I started the command line utility, but only got it far enough as needed to ease my testing. The compressor supports tons of options, but they're not yet exposed in the CLI tool. At the time I write this, the usage info is:
uglifyjs2 [options] input1.js [input2.js ...]
Maximum compression settings are on by default.
Use a single dash to read input from the standard input.
Options:
--source-map Specify an output file where to generate source map
--source-map-root The root of the original source to be included in the source map
-p, --prefix Skip prefix for original filenames that appear in source maps
-o, --output Output file (default STDOUT)
--stats Display operations run time on STDERR
-v, --verbose Verbose
-b, --beautify Beautify output
The CLI in v2 is able to take multiple input files; this was important for source maps. It compresses all input files, in sequence, and generates a single output to STDOUT (if --output wasn't passed) or to the file named with --output.
If the source map was requested it'll go in the file named with --source-map. The --source-map-root option allows you to specify an URL where to find the original source, and the --prefix option specifies the number of directories to drop. An example should clear any confusion. Here's how I'm minifying my DynarchLIB toolkit:
uglifyjs2 -p 5 \
--stats \
--source-map thelib.js.map \
--source-map-root "http://dynarchlib.local/" \
-o thelib.js \
`~/work/thelib/bin/list-scripts.pl`
The list-scripts.pl utility simply dumps to STDOUT the full paths of all scripts in the library, in proper order. The paths look like:
/home/mishoo/work/my/DynarchLIB/src/js/jslib.js
but the -p 5 means “drop 5 pieces from the prefix” (that's sort of like the -p in the “patch” utility), so the original source names in the source map will appear like src/js/jslib.js. When a browser will need to display the original source it'll compose the URL into http://dynarchlib.local/src/js/jslib.js
The CLI tool requires the optimist NodeJS module (available via NPM).
Support for source maps
This requires fitzgen's source-map library. It's available via NPM, though I installed it from Git.
My manual testing seems fine, but I wish there was a comprehensive way to test source maps (hints, anyone?).
Note that the source map is built in memory during code generation (the last step, after minification) and it kind of doubles the time spent in the code generator. You can check that by passing the --stats flag.
Compressor quality
I implemented already many of the ideas in UglifyJS v1, but it's still missing a few. Despite that, on my DynarchLIB, v2 is better than v1 by about 100 bytes (almost 400 bytes after gzip), which I'd say it's pretty good. It's also a lot faster.
Unfortunately, the test suite is lagging behind (but the fact that DL runs fine after compression gives me a lot of confidence that it's pretty stable; I also minified Esprima and ran its test suite, and it completed with 100% coverage.)
In the following sections I'll describe some of the things that UglifyJS2's compressor does, especially the “messy” parts.
Dead code removal and boolean context
Like v1, the new compressor does dead code removal (code after return, throw, break or continue) but it's a bit more aggressive. On rare occassions it might even break your code (though it's more likely that there's a hidden bug there). Here is one example:
if (false && x()) {
// dead code...
// the whole `if` will be dropped
}
The above will not affect code logic, but however, we also do the following transformation:
if (x() && false) {
// dead code...
// the whole `if` will be dropped
}
Again, we're dropping the whole if but we might have a problem: a JavaScript engine might in fact execute the x() call, even though the result of the condition is always false and the if body will never be executed. UglifyJS v2 drops the whole code and warns about it. I personally can't see any legitimate use for such expressions, but I'm very interested to hear more opinions!
Things that are treated as “known value” when in boolean context: (and by thing below I mean any expression)
thing + "string" ==> always true
"string" + thing ==> always true
typeof thing ==> always true
thing && false ==> always false
false && thing ==> always false
thing || true ==> always true
true || thing ==> always true
More could be added.
Another useful thing we can do if we're aware of the “boolean context”:
// v1:
if (!!foo) bar(); ==> !foo||bar()
// v2:
if (!!foo) bar(); ==> foo&&bar()
Note that in general !foo||bar()
is not the same as
foo&&bar()
, but in this particular case (where we know that the
foo expression is evaluated in boolean context) we can safely
compress that way, so v2 implements this minor optimization.
Hoisting of var / function
This was an optional feature in v1 (available via --lift-vars)
but v2 does it by default. Simply put, it'll get all names defined in a
function and put them in a single place; for example if you have var i
twice, the new version will leave a single declaration.
Now, if it can determine that a function doesn't use the arguments keyword, then it'll actually avoid inserting a var statement and declare the variables as function arguments instead. Example:
function foo(a, b) {
var x = a + b;
var y = x * x;
return y * y;
}
compresses to:
function foo(e,t,n,r){return n=e+t,r=n*n,r*r}
I'm pretty sure that's not a safe transformation in general, but for sane programs it should be OK. I'm considering moving it under an --unsafe flag though. Again, your opinions are welcome!
Other optimizations
Like the compressor in v1, v2 will apply various techniques to minify if statements, conditionals, resolve constant expressions, join consecutive statements into a “comma-operator” sequence, drop spurious block brackets etc. It's still not as good as v1 in some areas, but I think progress is nice and like I said, it already seems to beat v1 in terms of compression.
Lots of work left to do
I still have to implement the “unsafe” transformations that v1 does, and other compression options; need to expose those options in the command-line tool; implement the option to keep certain comments; need to write some documentation and more tests.
The campaign is still open.
Thank you for your support!
Install
Version 2 is not yet on NPM, but you can simply clone the repository and symlink the CLI utility (bin/uglifyjs2) into a directory in your $PATH.
Feedback
I'll be happy to receive any feedback on what's there so far. If you find a bug, please file a ticket at Github. If you want a new feature, you could file a ticket too, although chances are I won't care much about feature requests at this point—perhaps better to just drop a comment here, or email me, and let's talk about it.
Hire me
In closing, I should mention that I'm looking for a job. If you need an old-school hacker with solid knowledge and understanding of old and new technologies, I might be your guy. Please contact me.