Tag: ES6

ES6 Feature Performance

by on Jun.25, 2015, under Dev

Update: The results below are a snapshot from when this post was written. kpdecker.github.io/six-speed/ has the most recent results.

With ES6 features landing quickly in native browsers and readily available for use through Babel and Traceur, it seemed like it was time to look not just at support, but also the performance impact of using these features under the current implementations.

While there is great promise for the future, the picture of ES6 feature performance today is very muddled and depends on the specific feature being used. Some such as bindings and simple arrow/destructuring are ready for use today, others such as generators and tagged template strings might require analysis before using.

The standard warnings of premature optimization and recommendations to profile your own code apply to the comments here. These tests are very micro in their scope and might not be representative of your particular use case. It’s also possible that the ES6 version is fast enough for your use case and is not worth additional time spent refactoring to a more complicated but faster implementation.

Results

Arrow Function

Arrow functions invocation has little performance impact under transpilers. Their native implementation under Firefox is 40-70x slower for calls than the equivalent ES5 operation. Internet Explorer’s performance is approximately that of ES5.

Arrow function declaration on the other hand is slightly slower than the most optimized ES5 implementation, under most environments. Here too Firefox’s implementation shows a large performance hit and IE shows a slight performance hit.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
arrow tests babel 1.3x slower Identical Identical Identical Identical Identical Identical Identical Identical 1.2x faster Identical
traceur 1.3x slower Identical Identical Identical Identical Identical Identical Identical Identical 1.3x faster Identical
es5-bind 10x slower 15x slower 19x slower 14x slower 20x slower 6x slower 6x slower 7x slower 4x slower 2.6x slower 3x slower
es6 38x slower 41x slower 52x slower Identical
arrow-args tests babel Identical Identical Identical Identical Identical Identical Identical Identical Identical 1.2x faster Identical
traceur Identical Identical Identical Identical Identical Identical Identical Identical Identical 1.2x faster Identical
es6 2.9x slower 64x slower 89x slower 68x slower 1.3x faster
arrow-declare tests babel 1.3x slower 1.3x slower Identical Identical Identical 8x slower 10x slower 13x slower 2.3x slower 1.6x slower 1.4x slower
traceur 1.3x slower 1.3x slower Identical Identical Identical 5x slower 7x slower 8x slower 2.3x slower 1.4x slower 1.4x slower
es6 Identical 53x slower 78x slower 78x slower 1.3x slower

Issues:

Classes

With classes we start to see some differences in behaviors. Traceur and the V8 native implementation operate at partity with the ES5 tests when looking at instantiation. Babel’s implementation does suffer a 1.5-60x performance hit for the operations tested. When compiling using Babel’s loose mode, the hit is lessened to 8x.

The super keyword has some fairly large performance issues under all implementations, with the best case being 3x slower and the worst case being 60x slower than the respective baselines. V8’s native implementation also sees a 15-20x performance hit.

Babel’s loose implementation of super is akin to that of the ES5 implementation, utilizing C.prototype.bar.call(this) rather than the slower getPrototypeOf lookup operation that while more accurate technically, incurs an additional cost. This is controlled by the es6.classes parameter but Babel’s authors cite a number of warnings with this flag that may impact compatibility when migrating code to native implementations.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
classes tests babel 2.3x slower 1.5x slower 1.3x slower 1.4x slower 1.3x slower 27x slower 26x slower 30x slower 1.4x slower 1.5x slower Identical
babel-loose 2.3x slower 1.5x slower 1.3x slower 1.4x slower 1.3x slower 6x slower 6x slower 8x slower 1.3x slower 2.4x slower Identical
traceur Identical Identical Identical Identical Identical 1.5x slower 1.7x slower 1.8x slower Identical Identical Identical
es6 Identical Identical Identical Identical
super tests babel 24x slower 60x slower 52x slower 48x slower 45x slower 60x slower 62x slower 61x slower 24x slower 24x slower 16x slower
babel-loose 2.0x slower 1.7x slower 1.7x slower 1.7x slower 1.7x slower 5x slower 5x slower 5x slower 3x slower 3.0x slower 1.2x slower
traceur 12x slower 26x slower 18x slower 18x slower 18x slower 26x slower 30x slower 27x slower 10x slower 12x slower 11x slower
es6 14x slower 19x slower 21x slower 18x slower

Issues:

Enhanced Object Literals

Object literal extensions generally provide an overhead of up to 147x the baseline. Under the transpiler implementations this is due to the use of defineProperty rather than the much more optimized field assignment. This is done to bullet proof code from potential edge cases discussed here. Loose mode is effectively the same as the ES5 implementation as of 5.6.7

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
object-literal-ext tests babel 4x slower 60x slower 72x slower 71x slower 80x slower 1.8x slower 1.6x slower 1.6x slower 2.6x slower 2.0x slower 2.5x slower
babel-loose Identical 12x slower Identical Identical Identical Identical Identical Identical Identical Identical Identical
traceur 8x slower 120x slower 143x slower 122x slower 144x slower 3x slower 3x slower 2.9x slower 5x slower 4x slower 4x slower
es6 25x slower 23x slower 26x slower 1.4x slower 1.5x slower 1.3x slower Identical

Issues:

Template Strings

Template strings are a mixed bag. In the basic form, transpilers are able to hit parity with the baseline implementation under most environments. The native implementations are hit or miss. Under Chrome they execute at half the speed and under Firefox up to 650x slower.

Tagged template strings unfortunately do not have such a nice outlook. Their performance ranged from 2x slower for IE’s native implementation to 2000x slower for Babel’s implementation under Firefox. Babel’s loose implementation (es6.templateLiterals) lessens much of the overhead of this operation, at the cost of not having a fully compliant String.raw implementation.

Issues:

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
template_string tests babel Identical Identical Identical Identical Identical Identical Identical Identical 1.5x faster 1.4x faster Identical
traceur Identical Identical Identical Identical Identical Identical Identical Identical Identical Identical Identical
es6 Identical 2.0x slower 2.0x slower 1.9x slower 627x slower 622x slower 591x slower 1.3x faster
template_string_tag tests babel 567x slower 578x slower 722x slower 755x slower 820x slower 2395x slower 2349x slower 2028x slower 90x slower 82x slower 61x slower
babel-loose 2.0x slower 1.6x slower 1.7x slower 1.6x slower 1.8x slower 94x slower 87x slower 84x slower 1.4x slower 1.4x slower 2.0x slower
traceur 7x slower 13x slower 17x slower 13x slower 16x slower 346x slower 307x slower 261x slower 13x slower 11x slower 8x slower
es6 8x slower 9x slower 13x slower 9x slower 68x slower 64x slower 59x slower Identical

Destructuring

For destructuring, the average use case effectively matches that of the ES5 counterpart. Unfortunately complex use cases, particularly those around array destructuring, often have large performance overhead. Under Babel an unoptimized helper is used to access the data and under Traceur an entire iterator structure is created, both of which provide fairly substantial memory and CPU overhead over the simple array accessor logic that hand coded ES5 can utilize. Loose mode is effectively the same as the ES5 implementation.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
destructuring tests babel 1.8x slower 1.4x slower 1.4x slower 1.2x slower 1.3x slower 24x slower 22x slower 26x slower 3x slower 5x slower 2.2x slower
babel-loose 1.2x faster Identical Identical 1.2x faster Identical Identical Identical Identical Identical Identical Identical
traceur 26x slower 13x slower 12x slower 10x slower 11x slower 163x slower 152x slower 176x slower 57x slower 25x slower 8x slower
es6 170x slower 148x slower 185x slower Identical
destructuring-simple tests babel Identical Identical Identical Identical Identical Identical Identical Identical 10x slower 1.2x slower Identical
traceur Identical Identical Identical Identical Identical Identical Identical Identical Identical 1.3x slower Identical
es6 Identical Identical Identical Identical

Issues:

Default Parameters

Default parameters were universally slower for all transpiler implementations. They effectively compile to the same thing, utilizing the arguments object to set a local variable vs. using a named paramemter. This appears to be unoptimzed under all engines and consequently performance was 4-2000x slower. Sadly, this is required in order to properly implement the fn.length behavior defined by the spec:

NOTE The ExpectedArgumentCount of a FormalParameterList is the number of FormalParameters to the left of either the rest parameter or the first FormalParameter with an Initializer. A FormalParameter without an initializer is allowed after the first parameter with an initializer but such parameters are considered to be optional with undefined as their default value.

These scaled numbers should be taken in context. The ES5 equivalents are highly optimized, Firefox pushing over 833 million operations a second in one test, so the net performance of the transpiled versions may very well be sufficient for most use cases, particularly those not on the hot path.

The only native implementation, Firefox, performed identically to the ES5 implementation.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
classes tests babel 2.3x slower 1.5x slower 1.3x slower 1.4x slower 1.3x slower 27x slower 26x slower 30x slower 1.4x slower 1.5x slower Identical
babel-loose 2.3x slower 1.5x slower 1.3x slower 1.4x slower 1.3x slower 6x slower 6x slower 8x slower 1.3x slower 2.4x slower Identical
traceur Identical Identical Identical Identical Identical 1.5x slower 1.7x slower 1.8x slower Identical Identical Identical
es6 Identical Identical Identical Identical
defaults tests babel 17x slower 11x slower 11x slower 9x slower 8x slower 1842x slower 2051x slower 2043x slower 229x slower 72x slower 4x slower
traceur 16x slower 12x slower 12x slower 10x slower 9x slower 1759x slower 2305x slower 1974x slower 210x slower 73x slower 4x slower
es6 Identical Identical Identical

Rest Parameters

Rest parameters are as fast or faster than the ES5 equivalent under almost all implementations. Native implementations provided a performance boost up to 40x (with the exception of V8 where it causes a known deoptimization). Use them with a transpiler. They’re great. Death to arguments.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
rest tests babel Identical Identical Identical Identical Identical Identical Identical Identical 2.0x slower 1.5x slower Identical
traceur Identical 1.5x faster 1.2x faster 1.2x faster 1.2x faster 31x faster 34x faster 27x faster 2.4x slower 1.6x slower 1.3x faster
es6 3x slower 40x faster 42x faster 32x faster 8x faster

Spread Parameters

Under Babel, spread parameters for arrays perform identically to the ES5 counterpart as they are effectively both an apply call. Under Traceur and all of the native implementations the implementations are 1.3x to 17x slower.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
spread tests babel Identical Identical Identical Identical Identical Identical Identical Identical Identical Identical Identical
traceur 13x slower 6x slower 5x slower 6x slower 6x slower 2.7x slower 3x slower 3x slower 9x slower 712x slower 17x slower
es6 3x slower 4x slower 4x slower 1.3x slower 2.5x slower
spread-generator tests babel 150x slower 55x slower 58x slower 66x slower 71x slower 153x slower 143x slower 157x slower 656x slower 423x slower 64x slower
babel-loose 114x slower 46x slower 41x slower 45x slower 53x slower 60x slower 60x slower 61x slower 28x slower 51x slower 22x slower
traceur 27x slower 10x slower 9x slower 10x slower 13x slower 11x slower 10x slower 11x slower 15x slower 1551x slower 9x slower
es6 7x slower 8x slower 7x slower
spread-literal tests babel Identical Identical Identical Identical Identical 1.2x slower Identical 1.2x slower 3x slower 1.7x slower 2.0x slower
traceur 6x slower 3.0x slower 2.1x slower 2.5x slower 2.3x slower 6x slower 5x slower 5x slower 26x slower 451x slower 9x slower
es6 8x slower 7x slower 8x slower 1.8x slower 3x slower

Let + Const

let and const bindings were pretty much identical across the board. While these don’t offer performance improvements (yet), they shouldn’t negatively impact performance.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
bindings tests babel Identical Identical Identical Identical Identical Identical Identical Identical Identical Identical Identical
traceur Identical Identical Identical Identical Identical Identical Identical Identical Identical 1.3x faster Identical
es6 Identical Identical Identical Identical Identical 2.3x slower Identical Identical 1.3x slower Identical

For..of

for..of is universally slower, ranging from 3 to 20x slower for array iteration over classical array iteration. When iterating over an object with a custom iterator, the performance is also much slower than for..in iteration with hasOwnProperty checks.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
for-of-array tests babel 21x slower 8x slower 8x slower 9x slower 9x slower 14x slower 20x slower 19x slower 15x slower 489x slower 7x slower
babel-loose Identical Identical Identical Identical Identical 1.4x slower 1.7x slower 1.7x slower 1.3x slower Identical 1.8x slower
traceur 12x slower 7x slower 7x slower 8x slower 8x slower 26x slower 31x slower 32x slower 9x slower 6x slower 6x slower
es6 6x slower 6x slower 8x slower 8x slower 5x slower 7x slower 7x slower 2.9x slower 4x slower
for-of-object tests babel 10x slower 7x slower 8x slower 9x slower 6x slower 6x slower 6x slower 5x slower 60x slower 11x slower 6x slower
babel-loose 10x slower 6x slower 7x slower 9x slower 7x slower 4x slower 4x slower 4x slower 6x slower 449x slower 6x slower
traceur 8x slower 7x slower 8x slower 9x slower 7x slower 9x slower 10x slower 9x slower 5x slower 4x slower 4x slower
es6 6x slower 7x slower 8x slower 6x slower 3x slower 3x slower 3x slower 3x slower

Generators

Much like for..of, generators are also quite a bit slower than a raw ES5 implementation of the iterable protocol, with performance ranging from 10x to 750x slower. There is hope here as the V8 implementation achieves parity with the ES5 implementation.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
generator tests babel 601x slower 65x slower 67x slower 63x slower 69x slower 754x slower 723x slower 716x slower 79x slower 499x slower 78x slower
traceur 77x slower 10x slower 10x slower 10x slower 12x slower 48x slower 50x slower 46x slower 18x slower 112x slower 10x slower
es6 Identical 1.2x slower Identical Identical 22x slower 20x slower 19x slower

Issues:

Maps and Sets

Map and Set all have insert performance that is about 10x slower for a moderately sized data set. All of the implementations show massive improvement on the lookup operations, with Firefox’s native implementation showing a 200x speed increase for a dataset of size 500.

Traceur appears to delegate to the native implementation via their polyfill where possible so performance is closely linked to improvements in the native layer. In runtime mode, Babel does not appear to delegate and performance suffers as a result. Babel’s polyfill mode should behave as Traceur does but this was not directly tested.

Take caution with these numbers. The tests use a data set of size 500 and other data sets will have varying performance but it appears that these features are ready for general use if you have many reads and few writes or need to have objects as keys.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
map-set tests babel 13x slower 3.0x slower 5x slower 5x slower 3.0x slower 105x slower 12x slower 15x slower 60x slower 19x slower 79x slower
traceur 5x slower Identical 2.2x slower 2.3x slower 1.6x slower 20x slower 2.5x slower 2.4x slower 28x slower 2.2x slower 16x slower
es6 5x slower Identical 2.2x slower 2.3x slower 1.6x slower 21x slower 2.2x slower 2.7x slower 28x slower 2.2x slower 15x slower

Promises

Promises are across the board faster with both the polyfill and native implementations. This particular benchmark is dubious as it’s both async and inheriently tied to long running behaviors where execution overhead has little impact.

node chrome firefox internet explorer safari
0.10.39 2.3.0 43 44 45 38 39 40 11 12 8
promises tests babel Identical Identical 6x faster 7x faster 7x faster 21x faster 17x faster 16x faster Identical 1.2x slower 3x faster
traceur Identical Identical 2.0x faster 2.0x faster 1.9x faster 36x faster 31x faster Identical Identical 1.8x slower 3x faster
es6 Identical 2.0x faster 2.0x faster 1.9x faster 37x faster 29x faster Identical 1.9x slower 3x faster

Testing methodology

For each of the ES6 features in question, a ES5 implementation of that functionality was written along with a ES6 version. It should be noted that the functionality is frequently the same, but in some cases the “common” vs. “correct” version was written, i.e. using x[key] = value vs. defineProperty which is faster but can be hit but a particular nasty edge case for those who deem it fun to extend Object.prototype.

Babel, in both loose+runtime and runtime mode, and Traceur were then used to compile the ES6 version to a ES5 compliant version, utilizing the runtime over polyfill to maintain test isolation and avoid native implementations where possible.

All of these test instances were then benchmarked in the given JavaScript engine using Benchmark.js and then the operations per second compared to the ES5 implementation. Cross browser and cross execution comparisions are avoided as much as possible to isolate environmental issues when executing on VMs in the cloud.

All of this data, including any updates from more recent test runs is available at http://kpdecker.github.io/six-speed/ and the test suite is available at https://github.com/kpdecker/six-speed for review/feedback.

Takeaways

As noted above, these results might not be representative of your own application since they only test very small subsets of inputs and behaviors of these new features. If you are finding that you have performance issues with your code using these features, you should test them within your own environment to see what the actual behavior is.

While some of these features are a bit slow as of this writing, their performance should only improve as the native implementations mature and are optimized as real world use is applied to them. For the transpilers, there are some performance optimzations that can be made but much of the overhead they are experiencing is due to spec compliance, which they go to great lengths to achieve, but this comes with unfortunate overhead under ES5 implementations as they stand. Babel’s loose mode does offer a bit of a performance boost, but care must be taken when using loose mode as this could cause breakages when code is migrated to the standard native implementations.

Personally I intend to start using most of these features where they make sense but will avoid designing core APIs around these features (perhaps with the exception of Promises) until the native implementations have matured a bit.

4 Comments :, , more...

Visit our friends!

A few highly recommended friends...