JS Compilers Comparison
How Story Began
Every story begin with a bug...
While I was enjoying the mangle-props bring us almost 40% compression ratio, using the way I mentioned in my last blog post, everything goes well.
One day we decide to move our new UI components to React.js, I realize that I need to compile the JSX syntax before feeding them into compressor, Terser. Terser won't handle the JSX syntax minification, it's impossible for it to do so. Even you use babel-minify + syntax-jsx plugin has bug though.
I felt tired to apply this build process to our web apps. Sharing the build process as a tool or package is strange 'cause people its job it's much too unusual.
Babel (special syntaxes, e.g. JSX) --> Compress (terser-js) --> Babel (rest plugins)
You want above thing to become your bundler / build tool? I guess not.
Comparing Solutions
Basically there are several choices for us to transpile from ES6+ to ES5:
- Closure compiler
- Acorn
- Babel
- Typescript compiler (tsc)
- Bublé
- ... (there are more, but maybe not that popular)
Let's check them one by one
- Closure Compiler
heavy Java Dependencies. mangle-props supported, good but I got terser already. We want the easy way, without huge dependency, so goodbye to closure compiler first...
- Acorn
That's too raw, a parser. we need to write transform logic with acorn-walk
, babel already did transform work right? And babel learned from acorn, supported jsx, sounds not bad? Next move to babel.
Let's Really Try Them
Babel, Typescript and Buble sounds easy, they got their plugins for bundler (rollup / webpack loader) or they're configurable (ts and buble). We're easily to hack into that.
Babel
Let's express in brief what's the problem for babel. Example below is pretty simple:
ES6 syntax 1: class ES6 syntax 2: property accessor / mutator
class A {
method() {
console.log('method');
}
get x() { return 1 }
}
const B = {
get x() { return 2 }
}
We expected that transpiler convert them into pure ES5 code as simple as possible. The following snippet are the result from babel transpiler with ES2015 preset in loose mode.
var A =
/*#__PURE__*/
(function() {
function A() {}
var _proto = A.prototype;
_proto.method = function method() {
console.log("method");
};
_createClass(A, [
{
key: "x",
get: function get() {
return 1;
}
}
]);
return A;
})();
var B = {
get x() {
return 2;
}
};
Problem 1: cannot transpile the getters of object literal.
Problem 2: getter is quoted by babel in _createClass
helper, which means terser-js (mangle-props enabled) will end up with bug in that case.
For Problem 1: we could solve it with plugin @babel/plugin-transform-property-mutators which is not hard.
For Problem 2: How to hack the _createClass
helper stuff? Could we hack it before class transformed? Maybe!
Thoughts here: if we could move the setter / getter logic out of the _createClass
function, defined them in other ways like (Object.defineProperty), terser-js will whitelist these expression and won't mangle their names. Sounds good!
Can we do the similar stuff with any plugin? No. OK, let me write one.
I spent some time writing a babel plugin for solving Problem 2, finally I made babel-plugin-transform-class-property-mutators
With these plugins, the input like this
class A {
get x() {
return this._x || 1
}
}
will output right this way
Object.defineProperties(A, {
x: {
get: function () {
return A._x || 1;
},
configurable: true,
enumerable: true
}
})
See? getter / setter are defined in Object.defineProperties expression now, and it's ES5. Looks good? No, probably not.
Hey let's think about what we could do inside the class methods and property mutators? Class can derive from
parent, don't forget there is a keyword called super
and super.xxx
could be invoked inside setter / getter.
Then we need to compile the class derivation first! But plugins have order, if you do transform-class first, every thing is done then you won't have chance to run your plugin to hack anything.
Sad story, let's end with babel.
Typescript
Will ts become our preference? Typescript got really simple output with class transform, I made an example with both getter/setter and derivation. Here is the input:
example code
class P {
f() {
return 1
}
}
class Child extends P {
constructor() {
super()
}
get value() {
return super.f()
}
}
let instance = new Child()
console.log(instance.value)
The output look like this:
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var P = /** @class */ (function () {
function P() {
}
P.prototype.f = function () {
return 1;
};
return P;
}());
var Child = /** @class */ (function (_super) {
__extends(Child, _super);
function Child() {
return _super.call(this) || this;
}
Object.defineProperty(Child.prototype, "value", {
get: function () {
return _super.prototype.f.call(this);
},
enumerable: true,
configurable: true
});
return Child;
}(P));
var instance = new Child();
console.log(instance.value);
Looks great, that's perfect almost what we need! Almost? Not the exactly? Will tell you later...
Good news that I found some typescript plugins for rollup:
- https://github.com/ezolenko/rollup-plugin-typescript2
- https://github.com/rollup/rollup-plugin-typescript
And I integrate rollup-plugin-typescript2 with rollup, configure it for javascript files as well, turn off some specific options only for ts files (like generating types declaration, forcedly check typings...).
But later when I come to the config of them, found that they're almost only for typescript files. Not only because of some default configs.
// default config
{
include: ["*.ts+(|x)", "**/*.ts+(|x)"],
// ...
}
Developer will also be curious and confused that why something is just partially supported typescript, not fully set. Your install whole typescript package and just want it help you compile ES6 js to ES5. Is that over-designed?
I uninstalled typescript, during watching the Microsoft copyright in the generated compiled files.
Bublé
With our example code, checkout what will Bublé deal with it. You can play with it on the online repl web app: https://buble.surge.sh
Here we go
var P = function P () {};
P.prototype.f = function f () {
return 1
};
var Child = (function (P) {
function Child() {
P.call(this)
}
if ( P ) Child.__proto__ = P;
Child.prototype = Object.create( P && P.prototype );
Child.prototype.constructor = Child;
var prototypeAccessors = { value: { configurable: true } };
prototypeAccessors.value.get = function () {
return P.prototype.f.call(this)
};
Object.defineProperties( Child.prototype, prototypeAccessors );
return Child;
}(P));
var instance = new Child()
console.log(instance.value)
First: No string properties, everything defined on prototype or created by definedProperties
, great
Second: The generated code of class is really simple and short enough, I love the prototypeAccessors
Didn't see any issues here, the derivation is handled properly, the ES5 class is thin as well. I think that's the final choice.
Story Closed
Finally I ended up with leveraging buble as one of my rollup plugins, while building my bundler.
Unlike microbundle, I switched off the danger transforms of buble, just don't want to break any code while chasing the performance.
You can checkout the source code of bundler here: https://github.com/huozhi/bunchee.