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
The results below are a snapshot from when this post was written. kpdecker.github.io/six-speed/ has the most recent 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:
- [Firefox Native Implementation](https://bugzilla.mozilla.org/show_bug.cgi?id=1177518)
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:
- [V8](https://code.google.com/p/v8/issues/detail?id=3759)
- [Firefox Class Performance](https://bugzilla.mozilla.org/show_bug.cgi?id=1167472)
- [Firefox Super Property Performance](https://bugzilla.mozilla.org/show_bug.cgi?id=1169745)
- [Firefox Super Call Performance](https://bugzilla.mozilla.org/show_bug.cgi?id=1169746)
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:
- [Babel Optimization](https://github.com/babel/babel/pull/1830)
- [V8](https://code.google.com/p/v8/issues/detail?id=4246)
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:
- [Chrome](https://code.google.com/p/chromium/issues/detail?id=504212)
- [Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1177318)
- [Babel](https://github.com/babel/babel/issues/971#issuecomment-115344157)
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:
- [Firefox](https://bugzilla.mozilla.org/show_bug.cgi?id=1177319)
- [Babel Strict Optimization](https://github.com/babel/babel/pull/1821)
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:
- [Regenerator Init Optimization](https://github.com/facebook/regenerator/pull/207)
- [Regenerator Exec Optimization](https://github.com/facebook/regenerator/pull/208)
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.