JavaScript Performance

Introduction

JavaScript performance Top Gear I game screenshot

We all like to use fancy new language features. Except sometimes the new, fancy features are not the best choice for a given situation, and using some older (or less elegant, or frowned upon) feature ends up yielding better performance results.

Copy Array: push() vs …spread

Let’s do some a quick test to compare the performance difference when copying an array using Array.prototype.push() vs ...spread syntax and understand why spread is orders of magnitude less performant.

Setup

For this case-study, assume we have these setup lines in our .js file:

import { performance as perf } from 'node:perf_hooks';

const log = console.log.bind(console);

function toInt(n) {
  return n | 0;
}

Array of Random Numbers

We have a large array of numbers and want to make a copy of it. Should we use spread, which is the “new awesome idiomatic way”, or the “laughable, old-school, n00b style using push()”?

First, create an array of 10000 random numbers:

var nums = Array.apply(null, { length: 1e5 })
  .map(Function.call, Math.random);

push()

Let’s inspect the time it takes to copy the array using Array.prototype.push():

var pushIni = perf.now();

var _copyWithPush = nums.reduce((acc, n) => {
  acc.push(n);
  return acc;
}, []);

var pushEnd = perf.now();

log('PUSH:', toInt(pushEnd - pushIni));

Running the above “push()” snippet a bunch of times, it always took between 10 and 30 milliseconds on my current hardware.

Spread

Now, let’s inspect the time it takes to copy the array using spread:

var spreadIni = perf.now();

var copyWithSpread = nums.reduce((acc, n) => {
  return [...acc, n];
}, []);

var spreadEnd = perf.now();

log('SPREAD:', toInt(spreadEnd - spreadIni));

Running the above “spread” snippet a bunch of times, it always took more than a minute on my current hardware.

Explanation

Spreads are incredibly more costly than some other approaches that mutate existing data structures.

In the push() example, we only ever add one more value each time to the existing array.

With the spread approach, we always create a new array, copy the current existing values to it and add the new value. And it does this each time (or each iteration of the reduce), with ever increasing number of values to copy. Let me repeat: it copies and recopies an ever increasing number of values.

In hindsight, it is no surprise the the performance difference (time and space complexity) is almost unbelievably higher with spreads.

In many places, copying data, returning copy of data in functions, or passing copy of data to functions will not be a noticeable problem for very small pieces of data, but it can quickly become a performance problem for larger pieces of data or if used everywhere without some consideration.

Conclusion

Some languages have performant immutable data structures implementation, but that is not the case in ECMAScript. ECMAScript does not have immutable data structures. We sometimes make them immutable by applying some coding standards and principles (which are useful, sure), but that does not come cost-free.

Here’s the full code just in case:

//
// Make sure package.json contains "type": "module". Then,
// run the code with a command like this:
//
//   $ node --experimental-modules ./file.js
//

import { performance as perf } from 'node:perf_hooks';

const log = console.log.bind(console);

function toInt(n) {
  return n | 0;
}

////
// An array of 10000 numbers.
//
var nums = Array.apply(null, { length: 1e5 })
  .map(Function.call, Math.random);

////////////////////////////////////////////////////////////////////////
// push()
//
var pushIni = perf.now();

var numsCopy1 = nums.reduce((acc, n) => {
  acc.push(n);
  return acc;
}, []);

var pushEnd = perf.now();

log('PUSH:', toInt(pushEnd - pushIni));

////////////////////////////////////////////////////////////////////////
// spread
//
var spreadIni = perf.now();

var numsCopy2 = nums.reduce((acc, n) => {
  return [...acc, n];
}, []);

var spreadEnd = perf.now();

log('SPREAD:', toInt(spreadEnd - spreadIni));

Other Resources