UglifyJS

JS compressor of world fame.

Open demo

Latest blog entries tagged "uglifyjs"

/** May the source-map be with you! **/

UglifyJS — TreeTransformer

The tree transformer is a special case of a tree walker. In fact it even inherits from TreeWalker and you can use the same methods, but initialization and visitor protocol are a bit different.

SYNOPSIS

function before(node, descend) { ... };
function after(node) { ... };
var tt = new UglifyJS.TreeTransformer(before, after);
var new_ast = ast.transform(tt);

Both visitors are optional, but you should pass at least one of them (otherwise it'll just walk the tree pointlessly).

The “before” visitor

For each node in the tree, your before visitor is called before the children of that node are walked. It receives 2 arguments, the current node and the “descend” function.

The “after” visitor

If you provide an after visitor, it will be called on any node for which the before visitor returned undefined, after the node has been cloned and its children transformed. If your after visitor returns a value !== undefined, then that value will replace the current node in the tree (that's non-destructive, since the node was cloned first).

Examples

Hint: you can try these samples directly in the browser. Click the Ymacs icon (top-right when you hover the code). There press C-c C-e (CTRL-c followed by CTRL-e) to evaluate the code. Open the developer console in Firefox or Chrome to see the results of console.log.

Cloning a tree

A quick first example is cloning a tree. The UglifyJS compressor is destructive—it will modify the existing tree—so if you'd like to use the compressor but to also keep the original AST, you should make a clone of it. Here's how:

// sample AST
var ast = UglifyJS.parse("a = 1 + 2");

// this is the transformer
var deep_clone = new UglifyJS.TreeTransformer(function(node, descend){
    node = node.clone();
    // the descend function expects two arguments:
    // the node to dive into, and the tree walker
    // `this` here is the tree walker (=== deep_clone).
    // by descending into the *cloned* node, we keep the original intact
    descend(node, this);
    return node;
});

// in ast2 we'll get a deep copy of ast
var ast2 = ast.transform(deep_clone);

// let's change AST now:
ast.body[0].body.left.name = "CHANGED"; // CHANGED = 1 + 2

console.log(ast.print_to_string({ beautify: true }));
console.log(ast2.print_to_string({ beautify: true }));

A quick note, because the transformer itself will clone nodes if an “after” visitor is present, we could also clone a whole tree with the following (might even be a bit more efficient):

ast.transform(new UglifyJS.TreeTransformer(null, function(){}));

Consolidating strings

Let's say we'd like to collect all strings from a piece of code into a single var declaration, and use variables to access those strings. This should reduce size of code using a lot of strings, but only before Gzip (Gzip deals a lot better with string repetition than we can, so that's why UglifyJS doesn't do a similar transformation).

// in this hash we will map string to a variable name
var strings = {};

// here's the transformer:
var consolidate = new UglifyJS.TreeTransformer(null, function(node){
    if (node instanceof UglifyJS.AST_Toplevel) {
        // since we get here after the toplevel node was processed,
        // that means at the end, we'll just create the var definition,
        // or better yet, "const", and insert it as the first statement.
        var defs = new UglifyJS.AST_Const({
            definitions: Object.keys(strings).map(function(key){
                var x = strings[key];
                return new UglifyJS.AST_VarDef({
                    name  : new UglifyJS.AST_SymbolConst({ name: x.name }),
                    value : x.node, // the original AST_String
                });
            })
        });
        node.body.unshift(defs);
        return node;
    }
    if (node instanceof UglifyJS.AST_String) {
        // when we encounter a string, we give it an unique
        // variable name (see the getStringName function below)
        // and return a symbol reference instead.
        return new UglifyJS.AST_SymbolRef({
            start : node.start,
            end   : node.end,
            name  : getStringName(node).name,
        });
    }
});

var count = 0;
function getStringName(node) {
    var str = node.getValue(); // node is AST_String
    if (strings.hasOwnProperty(str)) return strings[str];
    var name = "_" + (++count);
    return strings[str] = { name: name, node: node };
}

// now let's try it.
var ast = UglifyJS.parse(function foo() {
    console.log("This is a string");
    console.log("Another string");
    console.log("Now repeat");
    var x = "This is a string", y = "Another string";
    var x = x + y + "Now repeat";
    alert("Now repeat".length);
    alert("Another string".length);
    alert("This is a string".length);
}.toString());

// transform and print
var ast2 = ast.transform(consolidate);
console.log(ast2.print_to_string({ beautify: true }));

// also, the change is non-destructive; the original AST remains the same:
console.log("Original:");
console.log(ast.print_to_string({ beautify: true }));

By not repeating the strings, this optimization could save quite a few bytes on code containing a lot of strings. However, as I said, it's not worthy because it ruins Gzip's own optimizations and will actually produce bigger code after Gzip.

Fork me on Github