ES6 transpilation via babel

Heads up, while profiling my code I came across a few slow downs due to the ES6 transpilation via babel, particularly relating to classes (I didn’t use that many ES6 features since I suspected there’d be performance hit). Suggest we all take heed to the following:

http://kpdecker.github.io/six-speed/

Things like classes, use of super(), etc in Chrome and FF range from 1.3x to 71x (!) slower. There are different ways of transpiling that can make some things faster and others slower. Browsers are also improving in their optimizations all the time. However, I think we need to have two assumptions:

  1. We are definitely targeting older browsers too (I don’t mean ancient, just not the latest)
  2. Even “1.3x” slower is a big deal for components which are running 10s of 1000s of times per second

Note there are a few things that are “faster”, and it might be worth noting those transpilation patterns for our own use too.

And of course, this doesn’t affect modules. So I’ll still be sticking to ES6 modules, jspm, etc which can be built for the other module systems too.

Oh also, in case it’s relevant for anyone else, my two gripes with ES6 classes (beyond performance):

  1. You can’t call the constructor() without new, which I guess makes perfect sense but allowed for some fun patterns in ES5.

  2. You can’t have properties (i.e. non functions) in classes. You can manually do Super.key = value but from what I recall this isn’t inherited and isn’t always accessible. I guess this encourages good getter/setter patterns, but again, what we could do in ES5 was convenient.

This is a great point. We have to make these performance decisions whether using ES5 or ES6 anyway. Any new library should be taking leverage of the future of Javascript.

It will be easy to determine what code will be running 1000s of times per second and manage the performance by leaving it in the faster ES5 code, because Babel will just ignore it on transpile.

Here is a great discussion on this, specifically Ben Newman’s reply about his talk with regards to Meteor and ES2015

Perhaps most importantly, any code that avoids using ES2015 features will have exactly the same generated code size and performance characteristics as ordinary ES5 code, because the compiler will just leave it untouched.

Since truly performance critical code tends to be isolated in relatively few hot spots, you always have the option of rewriting just those few bits of code in whatever style performs best, and otherwise reaping the benefits of the new language features throughout the overwhelming majority of your code. - Ben Newman

My take on using ES6 (really just common sense)

  • Don’t be afraid of classes. They promote good design patterns. Although be aware of the caveats of performance with respect to using them when creating large numbers of objects.
  • Keep with coding best practices, because you are not the only one maintaining the code. Clever design patterns just confuse everyone and the benefits are minimal.
  • Stay away from bad performing ES6 untill native support catches up.

Did you separate the Babel runtime? If not, then Babel will put duplicates of all it’s helpers into each module (file), which can be a lot of overhead.

(Too bad they didn’t put Coffeescript in there. :laughing: )

5 posts were split to a new topic: Browser support, share, usage, adoption, upgrades, etc

I think this is actually good, because properties on ES5 prototypes were singletons (unlike in Java), which means if you instantiated multiple objects from a class that had prototype properties, and modified those properties (suppose the properties were objects, and we modified things inside those objects) then the changes would apply for all instances of the class (unlike Java), allowing programmers to shoot themselves in the foot. I’d rather use static properties for that because it’s more readable and obvious what is intended, and let constructors define per-instance properties (like Java).

class Foo {
  constructor() {
    super()
    
    // per-instance properties
    this.foo = {foo: 'foo'}
    this.bar = {bar: 'bar'}
  }
}

Perhaps ES2016 or ES2017 will have the Java-like properties, which I think would be nice and would prevent newer programmers from making errors.

We can, of course, still use ES5 classes. There are libraries that let you do similar to the following:

var Foo = Class('Foo', {
  constructor() { ... },
  foo() { ... }
})

var Bar = Class('Bar').extends(Foo, {
  ...
})

I wonder how the use of a library like that would compare to Babel’s ES6 class performance (which is itself an ES3 implementation).

I was using ES6 classes with Famous 0.3 on my Nexus 5 phone, and it ran buttery smooth (unlike Mixed Mode).

Here are some prototype class libraries:

We could of course roll our own in like 20 min. We could also just agree to write them manually (like the classes in Famous).

Do we want to go the ES5-with-class-helper-library route, vanilla ES5 route, or ES6 class route? How fast will ES6 classes be when they are native, compared to ES5 classes? Can we live with the performance cost of ES5-implemented ES6 classes until they become native? Which method is likely to make it easier for beginner developers not to mess up and for things to just work?

ES6 for everything that does not have a real hit on performance, i.e Initialization/startup.
ES5 for the performance breaking pieces, but I would write it in ES6 first, then replace with ES5 later to check performance and run the benchmarks. This will promote a better standard and be ready to switch back as soon as native is faster. Either way you will be writing the two versions.

The developer would only be affected when using the API if they needed to do a deep dive into core code.

Note! You can just take the ES6 code, run it through the (Babel) transpiler and crop out the good stuff for the ES5 version.

Perhaps we can make a script for that.

Why is Babel’s implementation slower? Perhaps because it has the function-based helpers? It shouldn’t be that much slower.

Plus, class definitions happen before things run, so the class implementation shouldn’t really have so much effect at the time that animations are actually running, right?

Right! This would only be an issue when using a class like a lot of nodes, but if pooling correctly you only have this hit once.

You have to go look at the tests in the benchmarks to see what they are measuring. I really think it is a matter of doing our own benchmarks to know for sure.

But even so, the actual class definitions only happen at the very beginning; that’s where the function calls to the class helpers happen. At points where the constructor functions are called, does ES5 vs ES6 actually make a difference? My inclination would be that performance (time taken to instantiate classes) is almost equivalent at that point because the content of the constructor calls won’t change.

I’m just guessing. Let me see what they’re measuring…

I think these performance concerns are only really concerning in the interim until browsers update for ES6. We can target a release for when ES6 is in most browsers but until then take the perf hit, I think that is acceptable. Is traceur any more performant? IMHO we should go full speed with ES6 classes so later we don’t end up refactoring from class based library anyway.

1 Like

The perf hit is insignificant to me. I’m okay with using ES6 classes. The choice lies in the hands of others, and I’ll go either way. :]

just looked at the ES6 class tests: https://github.com/kpdecker/six-speed/tree/master/tests/classes

Those tests are small, and each one should actually be divided into two test: One test just defining a class over and over, and the other test instantiating a class over and over. IMO, only the results of the second test will matter.

Alright, I wrote a better test:

function testClassPerf() {
    let perfCounter
    let elapsed
    let before, after

    function A() {
      this.foo = 'bar';
    }
    A.prototype.bar = function() {
        console.log(this.foo)
    }

    before = performance.now()
    for (perfCounter=0; perfCounter < 100000; perfCounter+=1) {
        function A() {
          this.foo = 'bar';
        }
        A.prototype.bar = function() {
            console.log(this.foo)
        }
    }
    after = performance.now()
    console.log('definition test A: ' + (after-before) + 'ms')

    before = performance.now()
    for (perfCounter=0; perfCounter < 100000; perfCounter+=1) {
      new A()
    }
    after = performance.now()
    console.log('instantion test A: ' + (after-before) + 'ms')

    class B {
      constructor() {
        this.foo = 'bar';
      }
      bar() {
        console.log(this.foo)
      }
    }

    before = performance.now()
    for (perfCounter=0; perfCounter < 100000; perfCounter+=1) {
        class B {
          constructor() {
            this.foo = 'bar';
          }
          bar() {
            console.log(this.foo)
          }
        }
    }
    after = performance.now()
    console.log('definition test B: ' + (after-before) + 'ms')

    before = performance.now()
    for (perfCounter=0; perfCounter < 100000; perfCounter+=1) {
      new B()
    }
    after = performance.now()
    console.log('instantion test B: ' + (after-before) + 'ms')
}

I ran that in a Webpack+Babel project, so the actual source looks like this:

	function testClassPerf() {
	    var perfCounter = undefined;
	    var elapsed = undefined;
	    var before = undefined,
	        after = undefined;

	    function A() {
	        this.foo = 'bar';
	    }
	    A.prototype.bar = function () {
	        console.log(this.foo);
	    };

	    before = performance.now();
	    for (perfCounter = 0; perfCounter < 100000; perfCounter += 1) {
	        var _A = function _A() {
	            this.foo = 'bar';
	        };

	        _A.prototype.bar = function () {
	            console.log(this.foo);
	        };
	    }
	    after = performance.now();
	    console.log('definition test A: ' + (after - before) + 'ms');

	    before = performance.now();
	    for (perfCounter = 0; perfCounter < 100000; perfCounter += 1) {
	        new A();
	    }
	    after = performance.now();
	    console.log('instantion test A: ' + (after - before) + 'ms');

	    var B = (function () {
	        function B() {
	            _classCallCheck(this, B);

	            this.foo = 'bar';
	        }

	        _createClass(B, [{
	            key: 'bar',
	            value: function bar() {
	                console.log(this.foo);
	            }
	        }]);

	        return B;
	    })();

	    before = performance.now();

	    var _loop = function () {
	        var B = (function () {
	            function B() {
	                _classCallCheck(this, B);

	                this.foo = 'bar';
	            }

	            _createClass(B, [{
	                key: 'bar',
	                value: function bar() {
	                    console.log(this.foo);
	                }
	            }]);

	            return B;
	        })();
	    };

	    for (perfCounter = 0; perfCounter < 100000; perfCounter += 1) {
	        _loop();
	    }
	    after = performance.now();
	    console.log('definition test B: ' + (after - before) + 'ms');

	    before = performance.now();
	    for (perfCounter = 0; perfCounter < 100000; perfCounter += 1) {
	        new B();
	    }
	    after = performance.now();
	    console.log('instantion test B: ' + (after - before) + 'ms');
	}

Here are the results in Chromium 45.0.2454.85 (64-bit) for Linux, on a Macbook Pro 11,5:

definition test A: 224.46000000000004ms
instantion test A: 1.9549999999999272ms
definition test B: 657.0600000000004ms
instantion test B: 3.019999999999527ms

This tells us three things:

  • the time it takes to define classes is really slow, but in most cases, the user won’t be defining things conditionally (we’ll be defining classes in modules and exporting them which happens before the app runs (there are no conditional loads of modules in ES6 like in CommonJS, so we won’t be conditionally loading modules later than at the beginning (at least not with current Webpack and Browserify designs, but possibly in the future with ES6 Modules via HTTP/2 (see http://jspm.io for the first implementations of that)))).
  • the time it takes to construct instances is much faster.
  • the amount of time to instantiate 100,000 of these minimal instances, even with Babel’s version of ES5 classes (compiled from ES6 classes) being 33% slower, is negligible. The amount of time it’ll take to instantiate our engine nodes will be substantially greater than this by a whole lot.

Here’s how long it takes to instantiate 100,000 Famous Engine Nodes using the same technique:

    before = performance.now()
    for (perfCounter=0; perfCounter < 100000; perfCounter+=1) {
      new FamousNode()
    }
    after = performance.now()
    console.log('Famous Engine Node instantiation test: ' + (after-before) + 'ms')

result:

Famous Engine Node instantiation test: 114.11499999999978ms

I’d say it’s safe to use ES6 classes, performance wise.

1 Like

I also agree, and there are so many more reasons for using ES6 in terms of coding. BTW, great job on putting that together @trusktr!

1 Like

Oh, and it looks like ES7 (ES2016/ES2017?) might have property initializers, the following in (possible) ES7

class Foo {
  static foo = "foo"
  bar = "bar"
  static someMethod() {...}
  someOtherMethod() {...}
}

is equivalent to the following ES6:

class Foo {
  constructor() {
    super()
    this.bar = "bar"
  }
  someOtherMethod() {...}
}
Foo.someMethod = function someMethod() {...}
Foo.foo = "foo"

ES6 also already supports static methods.

Babel’s supports these features with an optional setting! It’s an optional setting because it hasn’t become an official part of the spec yet, but it looks like React and Babel are working together to push for it, so it might just happen.

I believe Babel does that. If you use WeakMap, it’ll let Chrome use it’s native one, otherwise it uses the polyfill.