Bind vs. Arrow Functions in Node.js

Sung Won Cho / @mikeswcho

Presented on April 9th 2017 at Sydney Node.ninjas

Structure

  1. Overview
  2. Comparison
  3. Conclusion

Overview

Compare arrow function and bind in node.js environment.

We will look at:

  • Memory footprint
  • Speed

Use cases

  • Providing `this` context
  • Currying

Providing `this` context


this.value = 'foo';

// Bind
var bound = (function() {
  return this.value;
}).bind(this);

// Arrow function
var arrow = () => {
  return this.value;
}

Useful when passing extracted method as callback:


						obj.on('error', connsole.log.bind(console));
						
vs.

						obj.on('error', (err) => { console.log(err); }));
						

Currying


// Given
function exp(a, b) {
  return a ** b;
}

// Bind
var expBase10 = exp.bind(null, 10);
expBase10(2); // => 100

// Arrow function
var expBase10 = (b) => exp(10, b);
expBase10(2); // => 100

Is one more performant than another?

Comparison

Memory footprint

Function.prototype.bind()


The bind() method creates a new function that, when called, has its this keyword set to the provided value ...

MDN

Methodology

  1. Record heap usage
  2. Allocate a function
  3. Record heap usage
  4. Calculate diff between 3 and 1

process.memoryUsage()

returns an object describing the memory usage of the Node.js process measured in bytes.


{
  rss: 4935680,
  heapTotal: 1826816,
  heapUsed: 650472, // <== V8's memory usage
  external: 49879
}
size_arrow.jsvar process = require('process');

this.name = 'node.ninjas';

var before = process.memoryUsage();

// Allocate arrow function
var fn = () => {
  return this.name;
}

var after = process.memoryUsage();

console.log("#### Arrow function")
console.log("## heap delta:",
	after.heapUsed - before.heapUsed)
size_bind.jsvar process = require('process');

this.name = 'node.ninjas';

var before = process.memoryUsage();

// Allocate bound function
var fn = function() {
  return this.name;
}.bind(this);

var after = process.memoryUsage();

console.log("#### Bound function")
console.log("## heap delta:",
	after.heapUsed - before.heapUsed)
size_demo

Result across multiple V8 versions

node V8 arrow bind
v0.12.10 v3.28.71.19 704 5256
v4.8.1 (Argon) v4.5.103.46 944 5496
v6.10.1 (Boron) v5.1.281.95 1144 1688
v7.7.4 v5.5.372.42 1208 1280

Tested on macOS Sierra v10.12.3

Summary - Memory

  • Arrow functions take less memory
  • Gap decreases in recent V8 engines

Comparison

Speed

  1. Allocation
  2. Invocation

Allocation

Methodology

  1. Warm up
  2. Measure allocation time 5000 times
  3. Remove outliers
  4. Get statistics

process.hrtime([time])

returns the current high-resolution real time in a `[seconds, nanoseconds]` array


var time = process.hrtime(); // [ 1800216, 25 ]

setTimeout(() => {
  var diff = process.hrtime(time); // [ 1, 552 ]
}, 1000);
speed_alloc_arrow.jsvar process = require('process');
var json2csv = require('json2csv');
var fs = require('fs');

this.name = 'node.ninjas';

// warmup
for (var i = 1; i < 10000; i++) {
  var fn = () => {
    return this.name;
  }
}

var diffs = [];
var n = 5000;
for (var i = 1; i < n; i++) {
  var start = process.hrtime();

  var fn = () => {
    return this.name;
  }

  var diff = process.hrtime(start)[1];
  diffs.push(diff)
}

var data = diffs.map(function(d) {
  return { time: d };
});
var csv = json2csv({ data: data, fields: [ 'time' ]})

fs.writeFile('speed_arrow.csv', csv, function(err) {
    if (err) throw err;
    console.log('file saved');
});
speed_alloc_bind.jsvar process = require('process');
var json2csv = require('json2csv');
var fs = require('fs');

this.name = 'node.ninjas';

// warmup
for (var i = 1; i < 10000; i++) {
	var fn = function() {
	  return this.name;
	}.bind(this);
}

var diffs = [];
var n = 5000;
for (var i = 1; i < n; i++) {
  var start = process.hrtime();

  var fn = function() {
    return this.name;
  }.bind(this);

  var diff = process.hrtime(start)[1];
  diffs.push(diff)
}

var data = diffs.map(function(d) {
  return { time: d };
});
var csv = json2csv({ data: data, fields: [ 'time' ]})

fs.writeFile('speed_bind.csv', csv, function(err) {
    if (err) throw err;
    console.log('file saved');
});
        
analyze.pyimport csv
import sys
import pandas as pd
import matplotlib.pyplot as plt
from numpy import std, mean, abs, median

values = []
filename = sys.argv[1]

with open(filename, 'rt') as f:
    reader = csv.DictReader(f)
    for row in reader:
        values.append(int(row['time']))

m = mean(values)
d = std(values)

def reject_outliers(x):
    if abs(x - m) <= d:
        return x

values = list(filter(reject_outliers, values))

print('min', min(values))
print('max', max(values))
print('mean', mean(values))
print('std', std(values))

df  = pd.read_csv("speed_arrow.csv")
df.plot()  # plots all columns against index
plt.show()
        

Result across multiple V8 versions

in (bind / arrow)

node V8 min max mean stddev
v0.12.10 v3.28.71.19 1068 / 560 25970 / 10072 1633 / 631 924 / 343
v4.8.1 (Argon) v4.5.103.46 821 / 586 12506 / 10440 1290 / 814 984 / 545
v6.10.1 (Boron) v5.1.281.95 99 /
95
4512 / 3670 120 / 112 111 / 70
v7.7.4 v5.5.372.42 97 /
94
3947 / 3796 135 / 136 130 / 136

Tested on macOS Sierra v10.12.3

Invocation

Methodology

  1. Warm up
  2. Call functions 5000 times and record time
  3. Remove outliers
  4. Get statistics
speedn_arrow_invocation.jsvar process = require('process');
var json2csv = require('json2csv');
var fs = require('fs');

this.name = 'node.ninjas';

var fn = () => {
  return this.name;
}

// warmup
for (var i = 1; i < 10000; i++) {
  fn();
}

var diffs = [];
var n = 5000;
for (var i = 1; i < n; i++) {
  var start = process.hrtime();

  fn();

  var diff = process.hrtime(start)[1];
  diffs.push(diff)
}

var data = diffs.map(function(d) {
  return { time: d };
});
var csv = json2csv({ data: data, fields: [ 'time' ]})

fs.writeFile('speed_arrow_invocation.csv', csv, function(err) {
    if (err) throw err;
    console.log('file saved');
});

speed_bind_invocation.jsvar process = require('process');
var json2csv = require('json2csv');
var fs = require('fs');

this.name = 'node.ninjas';

var fn = (function () {
  return this.name;
}).bind(this);

// warmup
for (var i = 1; i < 10000; i++) {
  fn();
}

var diffs = [];
var n = 5000;
for (var i = 1; i < n; i++) {
  var start = process.hrtime();

  fn();

  var diff = process.hrtime(start)[1];
  diffs.push(diff)
}

var data = diffs.map(function(d) {
  return { time: d };
});
var csv = json2csv({ data: data, fields: [ 'time' ]})

fs.writeFile('speed_bind_invocation.csv', csv, function(err) {
    if (err) throw err;
    console.log('file saved');
});

Result across multiple V8 versions

node V8 min max mean stddev
v0.12.10 v3.28.71.19 836 / 650 3775 / 2175 1679 / 1081 439 / 266
v4.8.1 (Argon) v4.5.103.46 838 / 615 2351 / 1827 1111 / 1045 406 / 425
v6.10.1 (Boron) v5.1.281.95 114 / 124 4252 / 4275 141 / 213 82 / 161
v7.7.4 v5.5.372.42 95 / 102 3401 / 3998 129 / 179 83 / 142

Tested on macOS Sierra v10.12.3

Summary - Speed

  • Faster to allocate arrow functions
  • Invocation speed - inconclusive

Conclusion

Across all node versions, arrow functions are generally faster.

Thanks