Javascript Benchmarking

In the last few weeks, I’ve been quite active on coding challenge sites such as Code Wars, Hacker Rank, and Code signal.

After solving a problem, you can check out the most popular solutions.
Seeing how another person solved a particular problem is exciting and allows you to grow as a developer. But if you look through the most upvoted solutions, you can see a pretty concerning pattern.

Usually, the most upvoted programming challenge answers are not the fastest ones but the shortest ones.

Since these sites do not show the performance of your code, I’ve used Benchmark.js and Jsperf to see the actual performance of some of the top voted solutions and methods that are frequently reoccurring.

Benchmark.js runs each function multiple times and then returns the operations per second value. The bigger the value, the faster the function.

Creating an Array from a String

As an example, let’s look at reversing a string.
Here are 3 different methods of doing the same thing.

1
2
3
4
5
6
7
8
9
10
11
// version 1

"hello".split("").reverse();

// version 2

[..."hello"].reverse();

// version 3

Array.from("hello").reverse();

In this example, the string hello is turned into a character array then reversed.

Let’s create a benchmark with random length arrays in Node.js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const { Benchmark } = require("benchmark");

function generateRandomString(length) {
var result = "";
var characters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

function generateRandomNumberBetween(min, max){
return Math.floor(Math.random() * max) + min;
}

// data contains 100 random strings with lneght between 1 and 1000
const data = [];
for (let i = 0; i < 100; i++) {
data.push(generateRandomString(generateRandomNumberBetween(1, 1000)));
}

const suite = new Benchmark.Suite();
suite.add("string.split()", function() {
for (const str of data) {
str.split("");
}
});
suite.add("Object spread", function () {
for (const str of data) {
[...str];
}
});
suite.add("Array.from()", function() {
for (const str of data) {
Array.from(str);
}
});
suite.on("cycle", function(event) {
console.log(String(event.target));
});
suite.on("complete", function() {
console.log("Fastest is " + this.filter("fastest").map("name"));
})
suite.run();

Result:

1
2
3
4
string.split() x 7,777 ops/sec ±16.99% (89 runs sampled)
Object spread x 284 ops/sec ±2.89% (77 runs sampled)
Array.from() x 138 ops/sec ±1.48% (75 runs sampled)
Fastest is string.split()

As you can see, the simple split method is almost 28 times faster then the Object spread method.

The test ran in Node.js on my laptop, but we can create a Jsperf test to validate the results in different browsers.
You can check out the test here.

Flattening an Array

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = [ [1, 2], [3, 4]];

// version 1

arr.reduce((acc, curr) => acc.concat(curr), []);

// version 2

arr.reduce((acc, curr) => [...acc, ...curr], []);

// version 3

[].concat(...arr);

Let’s compare them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const { Benchmark } = require("benchmark");

function generateRandomNumberBetween(min, max){
return Math.floor(Math.random() * max) + min;
}

function generateTupleArray(length) {
const tupleArray = [];
for (let i = 0; i < length; i++) {
tupleArray.push([generateRandomNumberBetween(1, 1e3), generateRandomNumberBetween(1, 1e3)]);
}
return tupleArray;
}

// Contains 100 arrays containing elements between 1 and 1000
const arrays = [];
for (let i = 0; i < 100; i++) {
arrays.push(generateTupleArray(generateRandomNumberBetween(1, 1e3)))
}


const suite = new Benchmark.Suite();
suite.add("acc.concat(curr)", function() {
for (const arr of arrays) {
arr.reduce((acc, curr) => acc.concat(curr), []);
}
});
suite.add("[...acc, ...curr]", function () {
for (const arr of arrays) {
arr.reduce((acc, curr) => [...acc, ...curr], []);
}
});
suite.add("[].concat(...data)", function() {
for (const arr of arrays) {
[].concat(...arr);
}
});
suite.on("cycle", function(event) {
console.log(String(event.target));
});
suite.on("complete", function() {
console.log("Fastest is " + this.filter("fastest").map("name"));
})
suite.run();

Result:

1
2
3
4
acc.concat(curr) x 11.13 ops/sec ±1.90% (32 runs sampled)
[...acc, ...curr] x 0.48 ops/sec ±9.61% (6 runs sampled)
[].concat(...data) x 442 ops/sec ±1.25% (85 runs sampled)
Fastest is [].concat(...data)

We can see that the fastest method is orders of magnitudes faster than the second one.

Partition problem

I’ve created a benchmark for the top 10 most upvoted answers for this particular problem.

You can check out the problem statement here

Result:

1
2
3
4
5
6
7
8
9
10
11
#1  x 4,288 ops/sec ±1.15% (87 runs sampled)
#2 x 11,726,715 ops/sec ±0.90% (88 runs sampled)
#3 x 25,793 ops/sec ±1.00% (88 runs sampled)
#4 x 15,749 ops/sec ±1.07% (86 runs sampled)
#5 x 144 ops/sec ±1.09% (79 runs sampled)
#6 x 8,761 ops/sec ±1.26% (86 runs sampled)
#7 x 1,021 ops/sec ±1.16% (84 runs sampled)
#8 x 4,574 ops/sec ±0.95% (88 runs sampled)
#9 x 181,853 ops/sec ±12.23% (81 runs sampled)
#10 x 20,143 ops/sec ±1.03% (83 runs sampled)
Fastest is #2

As you can see, the #1 most popular solution is one of the slowest.

Conclusion

Sometimes seemingly hacky solutions are viable. Other times they have similar or in worse cases, slower performance than standard solutions. Performance can vary between environments.

But one thing is for sure. If you want to make decisions based on data, you must know the tools to benchmark your code.