25

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?

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
Jan Grz
  • 1,373
  • 14
  • 18
  • Comments are not for extended discussion; this conversation has been [moved to chat](http://chat.stackoverflow.com/rooms/114723/discussion-on-question-by-jan-osch-why-is-let-slower-than-var-in-a-for-loop-in-n). – George Stocker Jun 15 '16 at 10:14
  • 1
    Issue on the V8 bug tracker: https://bugs.chromium.org/p/v8/issues/detail?id=4762&q=let%20label%3APerformance%20&colspec=ID%20Type%20Status%20Priority%20Owner%20Summary%20HW%20OS%20Component%20Stars – Jo Liss Mar 14 '17 at 17:59
  • 2
    Yes, testing in Chrome 60 this issue seems to solved in Chrome so it should be solved in node soon. It was already solved in SpiderMonkey. It has yet to be solved for Safari Nitro as of May 9th 2017 – gman May 09 '17 at 03:20
  • In the meantime, let is faster in Chrome, on Node, even in IE 11. – Wolfgang Stengel Feb 03 '21 at 10:19

2 Answers2

18

A note from the future: these historical performance differences are no longer accurate or relevant, as modern engines can optimize let semantics by using var semantics when there are no observable differences in behavior. When there are observable differences, using the correct semantics makes little difference in performance since the relevant code is already asynchronous in nature.

Based on the difference between the mechanics of var vs. let, this discrepancy in runtime is due 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.* see below 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);
  }
})();

Derek Ziemba brings up an interesting point:

Internet Explorer 14.14393 doesn't seem to have these [performance] issues.

Unfortunately, Internet Explorer incorrectly implemented let syntax by essentially using the simpler var semantics, so comparing its performance is a moot point:

In Internet Explorer, let within a for loop initializer does not create a separate variable for each loop iteration as defined by ES2015. Instead, it behaves as though the loop were wrapped in a scoping block with the let immediately before the loop.


* This transpiled version on Babel's REPL demonstrates what happens when you declare a let variable in a for loop. 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 proven by the values output within each closure.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • @PatrickRoberts your answer proves a different thing: that a variable declared with `var` remains in global scope: see `https://developer.mozilla.org/pl/docs/Web/JavaScript/Reference/Statements/let#let-scoped_variables_in_for_loops` – Jan Grz Jun 13 '16 at 15:28
  • @JanOsch: No, look closely at his `j` example and what it outputs. It's fascinating. [Here's Babel's transpilation of a simplified version.](http://babeljs.io/repl/#?evaluate=true&lineWrap=false&presets=es2015&code=for%20(let%20j%20%3D%200%3B%20j%20%3C%205%3B%20%2B%2Bj)%20%7B%0A%20%20setTimeout(function()%20%7B%0A%20%20%20%20console.log(j)%3B%0A%20%20%7D%2C%200)%3B%0A%7D) There are two aspects to the variable, one of which is maintained across loops, the other (which we see in the body) which is created each time. – T.J. Crowder Jun 13 '16 at 15:31
  • @JanOsch: If you're referring to *"...`var` exists in the entire block scope"*, I think by "block" he meant the "enclosing" scope of the block. The wording could be improved. –  Jun 13 '16 at 15:32
  • @squint wording improved. – Patrick Roberts Jun 13 '16 at 15:35
  • For anyone curious, see also the [Explanation of `let` and block scoping with for loops](http://stackoverflow.com/q/30899612/1048572) – Bergi Jun 14 '16 at 14:43
  • 1
    Internet Explorer 14.14393 doesn't seem to have these issues. It's 3x faster than Chrome 54 when looping with 'let', and 175% faster looping in general. http://jsbench.github.io/#e86c06909d7eedd18f8b9bac101e9d6d – Derek Ziemba Nov 04 '16 at 03:39
  • @DerekZiemba long overdue, but see my updated answer for an explanation. tl;dr IE is wrong (what a surprise) – Patrick Roberts Mar 16 '21 at 19:25
4

For this question. I try to find some clue form chrome V8 source code. here is the V8 loop peeling code:

https://github.com/v8/v8/blob/5.4.156/src/compiler/loop-peeling.cc

I try to understand it, I consider for loop has a middle layer in the implementation. for loop will hold the increment value in middle layer.

If loop use let to declare "i", V8 will declare a new variable i for every loop iterations, copy value of middle layer increment variable to that new declared "i", then put it to loop body scope;

If loop use var to declare "i", V8 will only put the middle layer increment value reference to the loop body scope. It will decrease the performance overhead of loop iteration.

Sorry for my pool english. There is a graph in the v8 source code, it will show you the mechanism.

Li Chunlin
  • 517
  • 3
  • 14