This document lists JavaScript language features and provides info with regard to their performance. In some cases it is explained why a feature used to be slow and how it was sped up.
The bottom line is that most features that could not be optimized previously due to limitations of crankshaft are now first class citizens of the new compiler chain and don't prevent optimizations anymore.
Therefore write clean idiomatic code as explained here, and use all features that the language provides.
Table of Contents generated with DocToc
- Function Bind
- instanceof and @@hasInstance
- Reflection API
- Array Builtins
- const
- Iterating Maps and Sets via
for of
- Iterating Maps and Sets via
forEach
and Callbacks - Iterating Object properties via for in
- Object Constructor Subclassing and Class Factories
- Tagged Templates
- Typed Arrays and ArrayBuffer
- Object.is
- Regular Expressions
- Destructuring
- Promises Async/Await
- Generators
- Proxies
- performance of
Function.prototype.bind
andbound
functions suffered from performance issues in crankshaft days - language boundaries C++/JS were crossed both ways which is expensive (esp. calling back from C++ into JS)
- two temporary arrays were created on every invocation of a bound function
- due to crankshaft limitations this couldn't be fixed easily there
- entirely new approach to how bound function exotic objects are implemented
- crossing C++/JS boundaries no longer needed
- pushing bound receiver and bound arguments directly and then calling target function allows further compile time optimizations and enables inlining the target function into the caller
- TurboFan inlines all mononomorphic calls to
bind
itself - resulted in ~400x speed improvement
- the performance of the React runtime, which makes heavy use of
bind
, doubled as a result
- developers should use bound functions freely wherever they apply without having to worry about performance penalties
- the two below snippets perform the same but arguably the second one is more readable and for the
case of
arr.reduce
is the only way to passthis
as it doesn't support passing it as a separate parameter likeforEach
andmap
do
// passing `this` to map as separate parameter
arr.map(convert, this)
// binding `this` to the convert function directly
arr.map(convert.bind(this))
- A new approach to Function.prototype.bind - 2015
- Optimizing bound functions further - 2016
- bound function exotic objects
- V8 release v6.4 - 2017
- latest JS allows overriding behavior of
instanceOf
via the@@hasInstance
well known symbol - naively this requires a check if
@@hasInstance
is defined for the given object every timeinstanceof
is invoked for it (in 99% of the cases it won't be defined) - initially that check was skipped as long as no overrides were added EVER (global protector cell)
- Node.js
Writable
class used@@hasInstance
and thus incurred huge performance bottleneck forinstanceof
~100x, since now checks were no longer skipped - optimizations weren't possible in these cases initially
- by avoiding to depend on global protector cell for TurboFan and allowing inlining
instancof
code this performance bottleneck has been fixed - similar improvements were made in similar fashion to other well-known symbols like
@@iterator
and@@toStringTag
- developers can use
instanceof
freely without worrying about non-deterministic performance characteristics - developers should think hard before overriding its behavior via
@@hasInstance
since this magical behavior may confuse others, but using it will incur no performance penalties
- V8: Behind the Scenes (November Edition) - 2016
- Investigating Performance of Object#toString in ES2015 - 2017
Reflect.apply
andReflect.construct
received 17x performance boost in V8 v6.1 and therefore should be considered performant at this point
-
Array
builtins likemap
,forEach
,reduce
,reduceRight
,find
,findIndex
,some
andevery
can be inlined into TurboFan optimized code which results in considerable performance improvement -
optimizations are applied to all major non-holey elements kinds for all
Array
builtins -
for all builtins, except
find
andfindIndex
holey floating-point arrays don't cause bailouts anymore
const
has more overhead when it comes to temporal deadzone related checks since it isn't hoisted- however the
const
keyword also guarantees that once a value is assigned to its slot it won't change in the future - as a result TurboFan skips loading and checking
const
slot values slots each time they are accessed (Function Context Specialization) - thus
const
improves performance, but only once the code was optimized
const
, likelet
adds cost due to TDZ (temporal deadzone) and thus performs slightly worse in unoptimized codeconst
performs a lot better in optimized code thanvar
orlet
for of
can be used to walk any collection that is iterable- this includes
Array
s,Map
s, andSet
s
- set iterators where implemented via a mix of self-hosted JavaScript and C++
- allocated two objects per iteration step (memory overhead -> increased GC work)
- transitioned between C++ and JS on every iteration step (expensive)
- additionally each
for of
is implicitly wrapped in atry/catch
block as per the language specification, which prevented its optimization due to crankshaft not ever optimizing functions which contained atry/catch
statement
- improved optimization of calls to
iterator.next()
- avoid allocation of
iterResult
via store-load propagation, escape analysis and scalar replacement of aggregates - avoid allocation of the iterator
- fully implemented in JavaScript via CodeStubAssembler
- only calls to C++ during GC
- full optimization now possible due to TurboFan's ability to optimize functions that include a
try/catch
statement
- use
for of
wherever needed without having to worry about performance cost
- both
Map
s andSet
s provide aforEach
method which allows iterating over it's items by providing a callback
- were mainly implemented in C++
- thus needed to transition to C++ first and to handle the callback needed to transition back to JavaScript (expensive)
forEach
builtins were ported to the CodeStubAssembler which lead to a significant performance improvement- since now no C++ is in play these function can further be optimized and inlined by TurboFan
- performance cost of using builtin
forEach
onMap
s andSet
s has been reduced drastically - however an additional closure is created which causes memory overhead
- the callback function is created new each time
forEach
is called (not for each item but each time we run that line of code) which could lead to it running in unoptimized mode - therefore when possible prefer
for of
construct as that doesn't need a callback function
var ownProps = 0
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) ownProps++
}
- problematic due to
obj.hasOwnProperty
call- may raise an error if
obj
was created viaObject.create(null)
obj.hasOwnProperty
becomes megamorphic ifobj
s with different shapes are passed
- may raise an error if
- better to replace that call with
Object.prototype.hasOwnProperty.call(obj, prop)
as it is safer and avoids potential performance hit
var ownProps = 0
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) ownProps++
}
- crankshaft applied two optimizations for cases were only enumerable fast properties on receiver were considered and prototype chain didn't contain enumerable properties or other special cases like proxies
- constant-folded
Object.hasOwnProperty
calls insidefor in
totrue
whenever possible, the below three conditions need to be met- object passed to call is identical to object we are enumerating
- object shape didn't change during loop iteration
- the passed key is the current enumerated property name
- enum cache indices were used to speed up property access
- enum cache needed to be adapted so TurboFan knew when it could safely use enum cache indices in order to avoid deoptimization loop (that also affected crankshaft)
- constant folding was ported to TurboFan
- separate KeyAccumulator was introduced to deal with complexities of collecting keys for
for-in
- KeyAccumulator consists of fast part which support limited set of
for-in
actions and slow part which supports all complex cases like ES6 Proxies - coupled with other TurboFan+Ignition advantages this led to ~60% speedup of the above case
for in
coupled with the correct use ofObject.prototype.hasOwnProperty.call(obj, prop)
is a very fast way to iterate over the properties of an object and thus should be used for these cases
- pure object subclassing
class A extends Object {}
by itself is not useful asclass B {}
will yield the same result even thoughclass A
's constructor will have different prototype chain thanclass B
's - however subclassing to
Object
is heavily used when implementing mixins via class factories - in the case that no base class is desired we pass
Object
as in the example below
function createClassBasedOn(BaseClass) {
return class Foo extends BaseClass { }
}
class Bar {}
const JustFoo = createClassBasedOn(Object)
const FooBar = createClassBasedOn(Bar)
- TurboFan detects the cases for which the
Object
constructor is used as the base class and fully inlines object instantiation
- class factories won't incur any extra overhead if no specific base class needs to be mixed
in and
Object
is passed to be extended from - therefore use freely wherever if mixins make sense
- tagged templates are optimized by TurboFan and can be used where they apply
- typed arrays are highly optimized by TurboFan
- calls to
Function.prototype.apply
with TypedArrays as a parameter were sped up which positively affected calls toString.fromCharCode
ArrayBuffer
view checks were improved by optimizingArrayBuffer.isView
andTypedArray.prototype[@@toStringTag]
- storing booleans inside TypedArrays was improved to where it now is identical to storing integers
- TypedArrays should be used wherever possible as it allows V8 to apply optimizations faster and more aggressively than for instance with plain Arrays
- any remaining bottlenecks will be fixed ASAP as TypedArrays being fast is a prerequisite of Webgl performing smoothly
- one usecase of
Object.is
is to check if a value is-0
viaObject.is(v, -0)
- previously implemented as C++ and thus couldn't be optimized
- now implemented via fast CodeStubAssembler which improved performance by ~14x
- migrated away from JavaScript to minimize overhead that hurt performance in previous implementation
- new design based on CodeStubAssembler
- entry-point stub into RegExp engine can easily be called from CodeStubAssembler
- make sure to neither modify the
RegExp
instance or its prototype as that will interfere with optimizations applied to regex operations - named capture groups are supported starting with V8 v6.4
- array destructuring performance on par with naive ES5 equivalent
- employ destructuring syntax freely in your applications
- native Promises in V8 have seen huge performance improvements as well as their use via
async/await
- V8 exposes C++ API allowing to trace through Promise lifecycle which is used by Node.js API to provide insight into Promise execution
- DevTools async stacktraces make Promise debugging a lot easier
- DevTools pause on exception breaks immediately when a Promise
reject
is invoked
- weren't optimizable in the past due to control flow limitations in Crankshaft
- new compiler chain generates bytecodes which de-sugar complex generator control flow into simpler local-control flow bytecodes
- these resulting bytecodes are easily optimized by TurboFan without knowing anything specific about generator control flow
- proxies required 4 jumps between C++ and JavaScript runtimes in the previous V8 compiler implementation
- porting C++ bits to CodeStubAssembler allows all execution to happen inside the JavaScript runtime, resulting in 0 jumps between runtimes
- this sped up numerous proxy operations
- while the use of proxies does incur an overhead, that overhead has been reduced drastically, but still should be avoided in hot code paths
- however use proxies whenever the problem you're trying to solve calls for it