Jan Osch Jan Osch - 4 months ago 18
Javascript Question

Why is let slower than var in a for loop in nodejs?

I have written a very simple benchmark:

console.time('var');
for (var i = 0; i < 100000000; i++) {}
console.timeEnd('var')


console.time('let');
for (let i = 0; i < 100000000; i++) {}
console.timeEnd('let')


If you're running Chrome, you can try it here (since NodeJS and Chrome use the same JavaScript engine, albeit usually slightly different versions):



// Since Node runs code in a function wrapper with a different
// `this` than global code, do that:
(function() {
console.time('var');
for (var i = 0; i < 100000000; i++) {}
console.timeEnd('var')


console.time('let');
for (let i = 0; i < 100000000; i++) {}
console.timeEnd('let')
}).call({});





And the results amaze me:

var: 89.162ms
let: 320.473ms


I have tested it in Node 4.0.0 && 5.0.0 && 6.0.0 and the proportion between
var
and
let
is the same for each node version.

Could someone please explain to me what is the reason behid this seemingly odd behaviour?

Answer

Based on the difference between the mechanics of var vs. let, it's related to the fact that var exists in the entire block scope of the anonymous function while let exists only within the loop and must be re-declared for each iteration.1 Here's an example demonstrating this point:

(function() {
  for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(`i: ${i} seconds`);
    }, i * 1000);
  }
  // 5, 5, 5, 5, 5


  for (let j = 0; j < 5; j++) {
    setTimeout(function() {
      console.log(`j: ${j} seconds`);
    }, 5000 + j * 1000);
  }
  // 0, 1, 2, 3, 4
}());

Notice that the i is shared across all iterations of the loop while let is not. Based on your benchmark, it appears that node.js just hasn't optimized scoping rules for let since it's much more recent and complicated than var is.

Elaboration

Here's a little layman explanation of let in for loops, for those who don't care to look into the admittedly dense specs, but are curious how let is re-declared for each iteration while still maintaining continuity.

But let can't possibly be re-declared for each iteration, because if you change it inside the loop, it propagates to the next iteration!

First here's an example that almost appears to validate this potential counter-argument:

(function() {
  for (let j = 0; j < 5; j++) {
    j++; // see how it skips 0, 2, and 4!?!?
    setTimeout(function() {
      console.log(`j: ${j} seconds`);
    }, j * 1000);
  }
}());

You are partially right, in that the changes respect the continuity of j. However, it is still re-declared for each iteration, as demonstrated by Babel:

"use strict";

(function () {
  var _loop = function _loop(_j) {
    _j++; // here's the change inside the new scope
    setTimeout(function () {
      console.log("j: " + _j + " seconds");
    }, _j * 1000);
    j = _j; // here's the change being propagated back to maintain continuity
  };

  for (var j = 0; j < 5; j++) {
    _loop(j);
  }
})();

Like it was said. Complicated rules. It's no wonder that a benchmark shows such a large discrepancy in performance (for now). Hopefully it will be further optimized in the future.


1: See this transpiled version on Babel's REPL to see this demonstrated. What happens when you declare a let variable in a for loop like that is that a new declarative environment is created to hold that variable (details here), and then for each loop iteration another declarative environment is created to hold a per-iteration copy of the variable; each iteration's copy is initialized from the previous one's value (details here), but they're separate variables, as demonstrated in the link by the values output by the closures.

Comments