ES6 transpilation via babel

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.

Does that rely on the underscore meaning super?

That’ be great to post onto the Babel GitHub issue tracker. If it’s transpiling to ES5, it should only be marginally slower, one would think. EDIT: https://github.com/babel/babel/issues/2399

Yeah, right! That’s not happening. x}

After seeing your findings, I’m totally down to go the pure ES5 route.

The loss of the satisfaction of how writing ES6 classes feels will be tiny compared to the gains in satisfaction from making the our library performant.

I never realized how under performing ES6 transpiling is today! I am kinda embarrassed that I never really thought about it until now. In truth, there has never been a need to look at it before now. Interesting to me is I even asked "why did WillyBoat not use ES6" to the willyboat team and not one answer from them.

We really need to think about how we are going to set this up to eventually go to the the faster code base between ES5 and ES6.

  • Is it safe to use ES6 modules?
  • should we still maintain an ES6 code base. Let the transpiler ignore the ES5 code and change when supported browser versions support native ES6?
  • would it be too much work to create the ES5 code from the transpile by stripping the wrappers? Always having the ES6 version ready?

I probably know the answers to most of these, but just want to throw them out in the case someone has a better idea than we come up with.

Edit: I have to take back some of my concern, because I was basing it on the tests above. Please see my test below. There should be no unexpected slow down of an extended class if written properly.

I asked Michael Obrien in person one time, and he said he loves ES6, but that performance would be out of their control.

Good to know that was the reason. Makes me feel better they came up with the same conclusion. Another example of documenting and being VERY clear about decisions, so we can just link to the explanation and not have to explain it :stuck_out_tongue:

I think so, because that only affects the code wrappers prior to any code running, so even if it takes a looooong time to load those modules, it won’t actually affect the runtime performance of the app.

Yeah, I don’t see why not. Certain things should be just fine (ES6 modules for example). We can write classes in a forward-compatible manner by using ES5 classes and never calling the super contructor after the use of this.

Which wrappers? The module wrappers? I don’t that really matters because they are all evaluated at the beginning (unless we’re using HTTP/2 with JSPM, but when that becomes an official way to load modules ES6 modules may already be native, that’s my guess).

In general, I think we can continue using ES6 for certain things, just not for class definitions. Some things compile to really simple ES5 with no function wrappers. We’ve gotta experiment/research to find out what exactly.

No. I meant the class wrappers in this case.

Yeah, I agree with supporting as much as possible and doing the rest as it goes native. Could even have both versions, to show the benchmark for why we are still there. That way we know when to make the change also.

I personally would first write my code in ES6, create my benchmark tests, then convert to the performance enhanced equivalent.

I don’t think the wrappers can be removed easily, but it’s possible. Someone would have to maintain that against Babel. That’d be cool to write ES6 classes and have it transpile to pure ES5 with no wrappers. It’s totally doable.

I don’t see why

class Foo extends Bar {
  someMethod() {...}
}

can’t simply become

function Foo() {
    Bar.apply(this, arguments)
}
Foo.prototype = Object.create(Bar.prototype)
Foo.prototype.constructor = Foo
Foo.prototype.someMethod = function someMethod() {...}

(in the simple case)

That’s a lot of extra work, unless we have a conversion script that converts to pure ES5 classes.

1 Like

@gadicc This was really bugging me. So I was trying to figure out why your tests would be so far off. 17x on a call to an extended function just made no sense to me. :stuck_out_tongue: Transpiling should not have affected it like it did.

I see now that the first test over complicated the use of super to create a false positive test. I kept telling myself these two instances should be returning exactly the same code values and getting the exact same reference values in the parent class. The only slow down would have been in the instantiating of the classes, IF we were testing that case.

new test returning exact same values:
http://jsperf.com/es6-classes-super-es5-vs-transpiled/8

ES6 code

    class Base {
      constructor(data) {
        this.data = data;
      }
      getData() {
        return this.data;
      }
    }

    class Sub extends Base {
       constructor(data) {
         super(data);
      }
    }

ES6 Transpiled

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Base = (function () {
  function Base(data) {
    _classCallCheck(this, Base);

    this.data = data;
  }

  Base.prototype.getData = function getData() {
    return this.data;
  };

  return Base;
})();

var Sub = (function (_Base) {
  _inherits(Sub, _Base);

  function Sub(data) {
    _classCallCheck(this, Sub);

    _Base.call(this, data);
  }

  return Sub;
})(Base);

ES5 code

    function ES5_Base(data) {
      this.data = data;
    }

    ES5_Base.prototype.getData = function() {
      return this.data;
    }

    function ES5_Sub(data) {
      ES5_Base.call(this, data);
    }

    ES5_Sub.prototype = Object.create(ES5_Base.prototype);
    ES5_Sub.prototype.constructor = ES5_Sub;

###Instances

this.es6t = new Sub(5);
this.es5 = new ES5_Sub(5);

###Test Cases

this.es6t.getData();
this.es5.getData();

You will be glad to know, I also increased the performance of the ES5 code at the same time as the ES6 code and they should be performing about twice as fast as the first example test.

I am going back to writing ES6 in most cases :smile:

Had anyone tested traceur compiler vs Babel? Traceur-runtime may not have some of these perf problems.