v8 JavaScript performance implications of const, let, and var?

TL;DR

In theory, an unoptimized version of this loop:

for (let i = 0; i < 500; ++i) {
    doSomethingWith(i);
}

might be slower than an unoptimized version of the same loop with var:

for (var i = 0; i < 500; ++i) {
    doSomethingWith(i);
}

because a different i variable is created for each loop iteration with let, whereas there’s only one i with var.

Arguing against that is the fact the var is hoisted so it’s declared outside the loop whereas the let is only declared within the loop, which may offer an optimization advantage.

In practice, here in 2018, modern JavaScript engines do enough introspection of the loop to know when it can optimize that difference away. (Even before then, odds are your loop was doing enough work that the additional let-related overhead was washed out anyway. But now you don’t even have to worry about it.)

Beware synthetic benchmarks as they are extremely easy to get wrong, and trigger JavaScript engine optimizers in ways that real code doesn’t (both good and bad ways). However, if you want a synthetic benchmark, here’s one:

const now = typeof performance === "object" && performance.now
    ? performance.now.bind(performance)
    : Date.now.bind(Date);

const btn = document.getElementById("btn");
btn.addEventListener("click", function() {
    btn.disabled = true;
    runTest();
});

const maxTests = 100;
const loopLimit = 50000000;
const expectedX = 1249999975000000;

function runTest(index = 1, results = {usingVar: 0, usingLet: 0}) {
    console.log(`Running Test #${index} of ${maxTests}`);
    setTimeout(() => {
        const varTime = usingVar();
        const letTime = usingLet();
        results.usingVar += varTime;
        results.usingLet += letTime;
        console.log(`Test ${index}: var = ${varTime}ms, let = ${letTime}ms`);
        ++index;
        if (index <= maxTests) {
            setTimeout(() => runTest(index, results), 0);
        } else {
            console.log(`Average time with var: ${(results.usingVar / maxTests).toFixed(2)}ms`);
            console.log(`Average time with let: ${(results.usingLet / maxTests).toFixed(2)}ms`);
            btn.disabled = false;
        }
    }, 0);
}

function usingVar() {
    const start = now();
    let x = 0;
    for (var i = 0; i < loopLimit; i++) {
        x += i;
    }
    if (x !== expectedX) {
        throw new Error("Error in test");
    }
    return now() - start;
}

function usingLet() {
    const start = now();
    let x = 0;
    for (let i = 0; i < loopLimit; i++) {
        x += i;
    }
    if (x !== expectedX) {
        throw new Error("Error in test");
    }
    return now() - start;
}
<input id="btn" type="button" value="Start">

It says that there’s no significant difference in that synthetic test on either V8/Chrome or SpiderMonkey/Firefox. (Repeated tests in both browsers have one winning, or the other winning, and in both cases within a margin of error.) But again, it’s a synthetic benchmark, not your code. Worry about the performance of your code when and if your code has a performance problem.

As a style matter, I prefer let for the scoping benefit and the closure-in-loops benefit if I use the loop variable in a closure.

Details

The important difference between var and let in a for loop is that a different i is created for each iteration; it addresses the classic “closures in loop” problem:

function usingVar() {
  for (var i = 0; i < 3; ++i) {
    setTimeout(function() {
      console.log("var's i: " + i);
    }, 0);
  }
}
function usingLet() {
  for (let i = 0; i < 3; ++i) {
    setTimeout(function() {
      console.log("let's i: " + i);
    }, 0);
  }
}
usingVar();
setTimeout(usingLet, 20);

Creating the new EnvironmentRecord for each loop body (spec link) is work, and work takes time, which is why in theory the let version is slower than the var version.

But the difference only matters if you create a function (closure) within the loop that uses i, as I did in that runnable snippet example above. Otherwise, the distinction can’t be observed and can be optimized away.

Here in 2018, it looks like V8 (and SpiderMonkey in Firefox) is doing sufficient introspection that there’s no performance cost in a loop that doesn’t make use of let‘s variable-per-iteration semantics. See this test.


In some cases, const may well provide an opportunity for optimization that var wouldn’t, especially for global variables.

The problem with a global variable is that it’s, well, global; any code anywhere could access it. So if you declare a variable with var that you never intend to change (and never do change in your code), the engine can’t assume it’s never going to change as the result of code loaded later or similar.

With const, though, you’re explicitly telling the engine that the value cannot change¹. So it’s free to do any optimization it wants, including emitting a literal instead of a variable reference to code using it, knowing that the values cannot be changed.

¹ Remember that with objects, the value is a reference to the object, not the object itself. So with const o = {}, you could change the state of the object (o.answer = 42), but you can’t make o point to a new object (because that would require changing the object reference it contains).


When using let or const in other var-like situations, they’re not likely to have different performance. This function should have exactly the same performance whether you use var or let, for instance:

function foo() {
    var i = 0;
    while (Math.random() < 0.5) {
        ++i;
    }
    return i;
}

It’s all, of course, unlikely to matter and something to worry about only if and when there’s a real problem to solve.

Leave a Comment