ES6 Tranceur vs Standard Javascript Performance
ES6 brings quite a few long-desired features Javascript engineers have been wanting for some time. Traceur is an open source tool for compiling ES6 into standard javascript and offers a compelling proposition - being able to write your javascript in ES6 and converting it to ES5 before shipping it. Four important points to evaluate when adopting technology is:
- Does it work?
- Is it robust?
- Is it reliable?
- How does it perform against what's out there and what you're currently using.
I've been considering writing more of my personal stuff in ES6 but wanted to evaluate performance against standard Javascript. This particular post is going to take a short look at #4 when put under a small stress test in Firefox. The tests will be against pre-compiled ES6, not run-time interpreted.
The Code
Standard Javascript:
function Factory(operation){
for(var n in operation) this[n] = operation[n];
this.units = [];
this.boxes = [];
};
Factory.prototype = {
startProduction: function(){
var _colors = ['black', 'white', 'silver', 'blue', 'red'],
_prices = [299.99, 299.99, 299.99, 349.99, 349.99],
_key = 0;
for(var i = 0; i < 100; i++){
_key = i%5;
this.make({
color: _colors[_key],
price: _prices[_key]
});
}
return this.package();
},
make: function(build){
this.units.push(new this.item({
brand: this.brand,
type : this.type,
color: build.color,
price: build.price,
tax : this.tax
}));
},
package: function(){
var _box = [];
for(var i = 1, l = this.units.length + 1; i < l; i++){
if(!(i%10)){
this.boxes.push(_box);
_box =[];
}
_box.push(this.units[i]);
};
return function(){
return this.boxes;
}.bind(this);
},
};
function Item(requirements){
for(var n in requirements) this[n] = requirements[n];
this.salesPrice = this.calculatePrice();
};
Item.prototype = {
calculatePrice: function(){
return this.price * (1 + this.tax);
}
};
var factory = new Factory({
brand: 'Sony',
type : 'Tablet',
tax : .11,
item : Item
});
for(var i = 0; i < 500; i++){
var output = factory.startProduction();
output();
}
ES6 Pre-compiled
class FactoryBase {
startProduction(){
var [_colors, _prices, _key] = [['black', 'white', 'silver', 'blue', 'red'], [299.99, 299.99, 299.99, 349.99, 349.99], 0];
for(let i = 0; i < 100; i++){
_key = i%5;
this.make({
color: _colors[_key],
price: _prices[_key]
});
}
return this.package();
}
make(build){
this.units.push(new this.item({
brand: this.brand,
type : this.type,
color: build.color,
price: build.price,
tax : this.tax
}));
}
'package'(){
var _box = [];
for(let i = 1, l = this.units.length + 1; i < l; i++){
if(!(i%10)){
this.boxes.push(_box);
_box =[];
}
_box.push(this.units[i]);
};
return () => {
return this.boxes;
}
}
};
class Factory extends FactoryBase{
constructor(operation){
for(let n in operation) this[n] = operation[n];
this.units = [];
this.boxes = [];
}
}
class ItemBase {
calculatePrice(){
return this.price * (1 + this.tax);
}
}
class Item extends ItemBase {
constructor(requirements){
for(var n in requirements) this[n] = requirements[n];
this.salesPrice = this.calculatePrice();
}
}
var factory = new Factory({
brand: 'Sony',
type : 'Tablet',
tax : .11,
item : Item
});
for(var i = 0; i < 500; i++){
var output = factory.startProduction();
output();
}
What these two scripts are doing is outputting an array of 10 arrays each with 10 instances of Item 500 times. The goal is to measure instantiation against ES6 classes, the effect of using block scoped bindings, destructured assignment and arrow functions. So how'd they perform?
The Results
At first glance your initial impression is probably going to be that you're never using Traceur. In this initial test it ran 550% slower than standard Javascript. So what gives? Well it turns out using "let" requires Traceur to implement some serious code to enforce scoping. That for loop in startProduction() gets converted to:
try {
throw undefined;
} catch ($i) {
$i = 0;
for (; $i < 100; $i++) {
try {
throw undefined;
} catch (i) {
i = $i;
try {
_key = i % 5;
this.make({
color: _colors[$traceurRuntime.toProperty(_key)],
price: _prices[$traceurRuntime.toProperty(_key)]
});
} finally {
$i = i;
}
}
}
}
}
This is most likely why block bindings aren't enabled by default and require an extra flag during compilation. Let's run the test again but swap out "let" with "var":
The results are looking much better this time around, only about twice as slow. The other features compile in a way that doesn't raise my eyebrow that it's effecting performance.
Conclusion
Looking at the results of these quick tests my opinion is performance-wise Traceur is not suitable for applications like games or anything heavily computational but should hold up fine for standard UI implementations and scripting. The main advantage in my eyes is that when the inevitable time comes to move everything over to ES6 you'll already be halfway to the finish line. Just be sure to avoid block bindings, it's nice to declare "let" but definitely not worth it until it's natively implemented.