Fast.js
Faster user-land reimplementations for several common builtin native JavaScript functions.
Install / Use
/learn @codemix/Fast.jsREADME
fast.js
Faster user-land reimplementations for several common builtin native JavaScript functions.
Note: fast.js is very young and in active development. The current version is optimised for V8 (chrome / node.js) and may not perform well in other JavaScript engines, so you may not want to use it in the browser at this point. Please read the caveats section before using fast.js.
What?
Fast.js is a collection of micro-optimisations aimed at making writing very fast JavaScript programs easier. It includes fast replacements for several built-in native methods such as .forEach, .map, .reduce etc, as well as common utility methods such as .clone.
Installation
Via npm:
npm install --save fast.js
Usage
var fast = require('fast.js');
console.log(fast.map([1,2,3], function (a) { return a * a; }));
How?
Thanks to advances in JavaScript engines such as V8 there is essentially no performance difference between native functions and their JavaScript equivalents, providing the developer is willing to go the extra mile to write very fast code. In fact, native functions often have to cover complicated edge cases from the ECMAScript specification, which put them at a performance disadvantage.
An example of such an edge case is sparse arrays and the .map, .reduce and .forEach functions:
var arr = new Array(100); // a sparse array with 100 slots
arr[20] = 'Hello World';
function logIt (item) {
console.log(item);
}
arr.forEach(logIt);
In the above example, the logIt function will be called only once, despite there being 100 slots in the array. This is because 99 of those slots are empty. To implement this behavior according to spec, the native forEach function must check whether each slot in the array has ever been assigned or not (a simple null or undefined check is not sufficient), and if so, the logIt function will be called.
However, almost no one actually uses this pattern - sparse arrays are very rare in the real world. But the native function must still perform this check, just in case. If we ignore the concept of sparse arrays completely, and pretend that they don't exist, we can write a JavaScript function which comfortably beats the native version:
var fast = require('fast.js');
var arr = [1,2,3,4,5];
fast.forEach(arr, logIt); // faster than arr.forEach(logIt)
By optimising for the 99% use case, fast.js methods can be up to 5x faster than their native equivalents.
Caveats
As mentioned above, fast.js does not conform 100% to the ECMAScript specification and is therefore not a drop in replacement 100% of the time. There are at least three scenarios where the behavior differs from the spec:
-
Sparse arrays are not supported. A sparse array will be treated just like a normal array, with unpopulated slots containing
undefinedvalues. This means that iteration functions such as.map()and.forEach()will visit these empty slots, receivingundefinedas an argument. This is in contrast to the native implementations where these unfilled slots will be skipped entirely by the iterators. In the real world, sparse arrays are very rare. This is evidenced by the very popular underscore.js's lack of support. -
Functions created using
fast.bind()andfast.partial()are not identical to functions created by the nativeFunction.prototype.bind(), specifically:-
The partial implementation creates functions that do not have immutable "poison pill" caller and arguments properties that throw a TypeError upon get, set, or deletion.
-
The partial implementation creates functions that have a prototype property. (Proper bound functions have none.)
-
The partial implementation creates bound functions whose length property does not agree with that mandated by ECMA-262: it creates functions with length 0, while a full implementation, depending on the length of the target function and the number of pre-specified arguments, may return a non-zero length.
See the documentation for
Function.prototype.bind()on MDN for more details. -
-
The behavior of
fast.reduce()differs from the nativeArray.prototype.reduce()in some important ways.-
Specifying an
undefinedinitialValueis the same as specifying no initial value at all. This differs from the spec which looks at the number of arguments specified. We just do a simple check forundefinedwhich may lead to unexpected results in some circumstances - if you're relying on the normal behavior of reduce when an initial value is specified, make sure that that value is notundefined. You can usually usenullas an alternative andnullwill not trigger this edge case. -
A 4th argument is supported -
thisContext, the context to bind the reducer function to. This is not present in the spec but is provided for convenience.
-
In practice, it's extremely unlikely that any of these caveats will have an impact on real world code. These constructs are extremely uncommon.
Benchmarks
To run the benchmarks in node.js:
npm run bench
To run the benchmarks in SpiderMonkey, you must first download js-shell. If you're on linux, this can be done by running:
npm run install-sm
This will download the latest nightly build of the js-shell binary and extract it to ci/environments/sm. If you're on mac or windows, you should download the appropriate build for your platform and place the extracted files in that directory.
After js-shell has been downloaded, you can run the SpiderMonkey benchmarks by running:
npm run bench-sm
Example benchmark output
> node ./bench/index.js
Running 55 benchmarks, please wait...
Native .fill() vs fast.fill() (3 items)
✓ Array.prototype.fill() x 19,878,369 ops/sec ±1.81% (90 runs sampled)
✓ fast.fill() x 160,929,619 ops/sec ±1.92% (89 runs sampled)
Result: fast.js is 709.57% faster than Array.prototype.fill().
Native .fill() vs fast.fill() (10 items)
✓ Array.prototype.fill() x 11,285,333 ops/sec ±2.13% (86 runs sampled)
✓ fast.fill() x 48,641,408 ops/sec ±4.87% (85 runs sampled)
Result: fast.js is 331.01% faster than Array.prototype.fill().
Native .fill() vs fast.fill() (1000 items)
✓ Array.prototype.fill() x 283,166 ops/sec ±1.65% (89 runs sampled)
✓ fast.fill() x 476,660 ops/sec ±1.43% (90 runs sampled)
Result: fast.js is 68.33% faster than Array.prototype.fill().
Native .reduce() plucker vs fast.pluck()
✓ Native Array::reduce() plucker x 1,041,300 ops/sec ±2.13% (87 runs sampled)
✓ fast.pluck() x 491,417 ops/sec ±0.97% (93 runs sampled)
✓ underscore.pluck() x 487,872 ops/sec ±1.06% (92 runs sampled)
✓ lodash.pluck():
Result: fast.js is 52.81% slower than Native Array::reduce() plucker.
Native Object.keys().map() value extractor vs fast.values()
✓ Native Object.keys().map() x 5,435,909 ops/sec ±1.47% (90 runs sampled)
✓ fast.values() x 11,500,439 ops/sec ±2.18% (83 runs sampled)
✓ underscore.values() x 5,543,090 ops/sec ±1.41% (91 runs sampled)
✓ lodash.values() x 4,081,797 ops/sec ±1.55% (90 runs sampled)
Result: fast.js is 111.56% faster than Native Object.keys().map().
Object.assign() vs fast.assign()
✓ Object.assign() x 250,190 ops/sec ±1.36% (93 runs sampled)
✓ fast.assign() x 208,612 ops/sec ±1.44% (87 runs sampled)
✓ fast.assign() v0.0.4c x 212,198 ops/sec ±1.67% (87 runs sampled)
✓ fast.assign() v0.0.4b x 197,658 ops/sec ±1.34% (89 runs sampled)
✓ lodash.assign() x 163,550 ops/sec ±1.34% (87 runs sampled)
Result: fast.js is 16.62% slower than Object.assign().
Object.assign() vs fast.assign() (3 arguments)
✓ Object.assign() x 81,027 ops/sec ±1.61% (88 runs sampled)
✓ fast.assign() x 72,334 ops/sec ±1.06% (91 runs sampled)
✓ fast.assign() v0.0.4c x 73,304 ops/sec ±1.05% (87 runs sampled)
✓ fast.assign() v0.0.4b x 63,523 ops/sec ±1.22% (92 runs sampled)
Result: fast.js is 10.73% slower than Object.assign().
Object.assign() vs fast.assign() (10 arguments)
✓ Object.assign() x 25,287 ops/sec ±3.27% (80 runs sampled)
✓ fast.assign() x 24,588 ops/sec ±1.63% (91 runs sampled)
✓ fast.assign() v0.0.4c x 24,207 ops/sec ±2.34% (87 runs sampled)
✓ fast.assign() v0.0.4b x 20,558 ops/sec ±2.15% (85 runs sampled)
Result: fast.js is 2.77% slower than Object.assign().
Native string comparison vs fast.intern() (short)
✓ Native comparison x 85,392,447 ops/sec ±21.72% (89 runs sampled)
✓ fast.intern() x 184,548,307 ops/sec ±0.59% (99 runs sampled)
Result: fast.js is 116.12% faster than Native comparison.
Native string comparison vs fast.intern() (medium)
✓ Native comparison x 3,761,682 ops/sec ±20.61% (95 runs sampled)
✓ fast.intern() x 182,842,946 ops/sec ±0.77% (97 runs sampled)
Result: fast.js is 4760.67% faster than Native comparison.
Native string comparison vs fast.intern() (long)
✓ Native comparison x 19,951 ops/sec ±0.63% (97 runs sampled)
✓ fast.intern() x 179,058,362 ops/sec ±1.58% (94 runs sampled)
Result: fast.js is 897376.33% faster than Native comparison.
Native try {} catch (e) {} vs fast.try()
✓ try...catch x 163,886 ops/sec ±1.13% (93 runs sampled)
✓ fast.try() x 2,886,759 ops/sec ±1.93% (86 runs sampled)
Result: fast.js is 1661.44% faster than try...catch.
Native try {} catch (e) {} vs fast.try() (single f
