Does spread operator affect performance?

In case someone is stumbling uppon this question while wondering about array spread operations instead of objects:

I benched different methods to accomplish:

const clone = [...original]

var original = [];
var clone = [];

for (var i = 0; i < 10000000; i++) {
    original.push(1);
}
var cycle = 0;

var spreadTime = [];
var mapTime = [];
var forTime = [];
var reduceTime = [];
var sliceTime = [];
var arrayFromTime = [];

while (cycle < 10) {
  var d = Date.now();
  clone = [];
  clone = [...original];
  spreadTime.push(Date.now() - d);

  d = Date.now();
  clone = [];
  clone = original.map((entry) => entry);
  mapTime.push(Date.now() - d);

  d = Date.now();
  clone = [];
  for (var i = 0; i < original.length; i++) {
      clone[i] = original[i];
  }
  forTime.push(Date.now() - d);

  d = Date.now();
  clone = [];
  clone = original.reduce((next, e) => {
      next.push(e);

      return next;
  }, []);
  reduceTime.push(Date.now() - d);
  
  d = Date.now();
  clone = [];
  clone = original.slice();
  sliceTime.push(Date.now() - d);

  d = Date.now();
  clone = [];
  clone = Array.from(original);
  arrayFromTime.push(Date.now() - d);

  cycle ++;
  document.getElementById("cycle").innerHTML = cycle;
  document.getElementById("spreadTime").innerHTML = spreadTime.reduce((a,b) => a + b, 0) / spreadTime.length;
  document.getElementById("mapTime").innerHTML = mapTime.reduce((a,b) => a + b, 0) / mapTime.length;
  document.getElementById("forTime").innerHTML = forTime.reduce((a,b) => a + b, 0) / forTime.length;
  document.getElementById("reduceTime").innerHTML = reduceTime.reduce((a,b) => a + b, 0) / reduceTime.length;
  document.getElementById("sliceTime").innerHTML = sliceTime.reduce((a,b) => a + b, 0) / sliceTime.length;
  document.getElementById("arrayFromTime").innerHTML = arrayFromTime.reduce((a,b) => a + b, 0) / arrayFromTime.length;
}
<View>
  <h1>cycle <span id="cycle"></span></h1>
  spread: <span id="spreadTime"></span> ms
  <br/>
  map: <span id="mapTime"></span> ms
  <br/>
  for: <span id="forTime"></span> ms
  <br/>
  reduce: <span id="reduceTime"></span> ms
  <br/>
  slice: <span id="sliceTime"></span> ms
  <br/>
  arrayFrom: <span id="arrayFromTime"></span> ms
  <br/>
</View>

Yes, spreading a variable which refers to an object into another object requires the interpreter to look up what the variable refers to, and then look up all the enumerable own properties (and the associated values) of the object that gets spreaded so as to insert into the new object. This does indeed take a bit of processing power.

But, on modern computers, and on modern JS engines, the processing power required is next to nothing; what does it matter, when millions of instructions can be processed each second? A handful of key-value pairs is nothing to worry about.

Unless you've identified that you're spreading an object with tons of key-value pairs, and it's actually causing a performance bottleneck, it would be a better idea to avoid premature optimization and aim to write clean, readable code instead (which may well invoke using spread syntax often). For a large employees array, the second approach is more readable than the first.

(though, you also might consider using .map, to keep the code even DRY-er:)

const employeesInitial = [
  {
    employeeId: 123,
    employeeName: 'p'
  },
  {
    employeeId: 456,
    employeeName: 'q'
  },
  {
    employeeId: 789,
    employeeName: 'r'
  }
];
const employees = employeesInitial.map((obj) => ({ ...obj, ...commonParams }));

The cost of spreading is significant. We're talking 2 orders of magnitude here.

const { x, y } = z

z = { x, y: y + 1 } // faster
z = { ...z, y: y + 1 } // slower

While they both accomplish similar things they are very different in their performance characteristics. But it will depend, if and how your JavaScript is transpiled.

For example, Babel will actually emit something which is similar to the faster variant if you target ES2015 but if you target ES2017 you'll get the slower variant, as-is. If you target ECMASCRIPT_2018 with the Google Closure Compiler you get the slower variant. With the TypeScript compiler you end up with twice as many objects because it does nested Object.assign calls.

While spreading is slower you're still getting a lot of ops per second. It's just that if you do it the boring way you'll get a lot more ops per second.

I put together a jsperf example to illustrate this.

https://jsperf.com/the-cost-of-spreading/1

If you have a hot code path that does spreading, consider direct construction. Otherwise, don't bother.


Time to run second approach will be longer (even if very little on modern computers) as interpreter has to iterate over keys of commonParams and copy them to each object.

Wrote a benchmark to find difference which is almost zero for small objects.

function runFirstApproach(){
  const employees1 = [
    {
      company: 'ABC',
      country: 'IN',
      zip: 123,
      employeeId: 123,
      employeeName: 'p'
    },
    {
      company: 'ABC',
      country: 'IN',
      zip: 123,
      employeeId: 456,
      employeeName: 'q'
    },
    {
      company: 'ABC',
      country: 'IN',
      zip: 123,
      employeeId: 789,
      employeeName: 'r'
    }
  ];
}

function runSecondApproach() {
  const commonParams = {
    company: 'ABC',
    country: 'IN',
    zip: 123
  };

  const employees2 = [
    {
      ...commonParams,
      employeeId: 123,
      employeeName: 'p'
    },
    {
      ...commonParams,
      employeeId: 456,
      employeeName: 'q'
    },
    {
      ...commonParams,
      employeeId: 789,
      employeeName: 'r'
    }
  ]
}

function runBenchmarkWithFirstApproach(){
  console.log("Avg time to run first approach -> ", getAvgRunTime(runFirstApproach, 100000))
}

function runBenchmarkWithSecondApproach(){
  console.log("Avg time to run second approach ->", getAvgRunTime(runSecondApproach, 100000))
}

function getAvgRunTime(func, rep){
  let totalTime = 0;
  let tempRep = rep;
  while(tempRep--) {
    const startTime = Date.now();
    func();
    const endTime = Date.now();
    const timeTaken = endTime-startTime;
    totalTime += timeTaken;
  }
  return totalTime/rep;
}

runBenchmarkWithFirstApproach();
runBenchmarkWithSecondApproach();