-2

I'm trying to understand why arrow functions might run faster or slower in different javascript contexts.

Given the following three options:

  1. JS Statement
  • (e.g. paragraph.textContent = 'Loop iteration ' + (i + 1))
  1. JS Anonymous Arrow Function
  • (e.g. (i) => paragraph.textContent = 'Loop iteration ' + (i + 1))
  1. JS Arrow Function assigned to Named Variable
  • (e.g. myFunction(i))

I would have guessed (entirely wrongly) that the raw JS Statement might run the fastest, followed by the Arrow Function assigned to Named Variable, and that the Anonymous Arrow Function might run the slowest.

So I wrote a quick to test to verify that my assumptions were correct:

let paragraph = document.querySelector('p');
let button = document.querySelector('button');

let rawStatement = document.querySelector('.rawStatement');
let anonymousFunction = document.querySelector('.anonymousFunction');
let functionAssignedToVariable = document.querySelector('.functionAssignedToVariable');

const runRawStatement = () => {

  let scriptDurations = [];
  
  for (let h = 0; h < 100; h++) {
    
    let scriptStart = window.performance.now();
    
    for (let i = 0; i < 10000; i++) {
      paragraph.textContent = 'Loop iteration ' + (i + 1);
    }
    
    let scriptEnd = window.performance.now();
    
    scriptDurations.push((scriptEnd - scriptStart));
  }
    
  return ((scriptDurations.reduce((a, b) => a + b, 0)) / 100);
}

const runAnonymousFunction = () => {

  let scriptDurations = [];

  for (let h = 0; h < 10; h++) {

    let scriptStart = window.performance.now();

    for (let i = 0; i < 10000; i++) {
      ((i) => paragraph.textContent = 'Loop iteration ' + (i + 1))();
    }

    let scriptEnd = window.performance.now();

    scriptDurations.push((scriptEnd - scriptStart));
  }

  return ((scriptDurations.reduce((a, b) => a + b, 0)) / 10);
}

const runFunctionAssignedToVariable = () => {

  let scriptDurations = [];

  for (let h = 0; h < 10; h++) {

    const myFunction = (i) => paragraph.textContent = 'Loop iteration ' + (i + 1);

    let scriptStart = window.performance.now();

    for (let i = 0; i < 10000; i++) {
      myFunction(i);
    }

    let scriptEnd = window.performance.now();

    scriptDurations.push((scriptEnd - scriptStart));
  }

  return ((scriptDurations.reduce((a, b) => a + b, 0)) / 10);
}

const runTimers = () => {

  let runRawStatementDuration = runRawStatement();
  document.querySelector('.rawStatement').textContent = runRawStatementDuration + 'ms';
  
  let runRawFunctionDuration = runAnonymousFunction();
  document.querySelector('.anonymousFunction').textContent = runRawFunctionDuration + 'ms';
    
  let runVariableFunctionDuration = runFunctionAssignedToVariable();
  document.querySelector('.functionAssignedToVariable').textContent = runVariableFunctionDuration + 'ms';
  
  button.textContent = 'Re-run Script';
}
  
button.addEventListener('click', runTimers, false);
button {
  display: inline-block;
  cursor: pointer;
}

table {
  margin: 12px 0;
  border-collapse: collapse;
}

th,
td {
  padding: 6px;
  border: 1px solid rgb(0, 0, 0);
}

button {
  display: inline-block;
  margin-right: 6px;
}
<table>
<thead><th>Script Type</th><th>Average of 1000 run-throughs</th></thead>
<tr><td>JS Statement</td><td class="rawStatement"></td></tr>
<tr><td>Anonymous Arrow Function</td><td class="anonymousFunction"></td></tr>
<tr><td>Arrow Function assigned to Named Variable</td><td class="functionAssignedToVariable"></td></tr>
</table>

<button type="button">Run Script</button>
<em>(N.B. script takes about 5 seconds to run)</em>

<p></p>

However, consistently, the anonymous function is the fastest - by a degree of magnitude - followed (a long way behind) by the same anonymous function assigned to a variable and the slowest code to run is usually the raw JS statement.

Can anyone explain why the results consistently show that the anonymous function runs so much more slowly when assigned to a variable and why both the functions are faster than the raw JS statement.

Rounin
  • 27,134
  • 9
  • 83
  • 108
  • 7
    `however, consistently, the anonymous function is the fastest (by a degree of magnitude)` that part's easy: you never call the anonymous function (you just create it), so there is much less code being run. For a better comparison, change that to: `((i) => paragraph.textContent = 'Loop iteration ' + (i + 1))(i)` – Nicholas Tower Dec 26 '22 at 15:49
  • Thank you, @NicholasTower, that was extremely helpful. I've updated the test above and it's now giving three similar results - which is much more in line with what I was initially expecting. – Rounin Dec 26 '22 at 17:06
  • In fact, @NicholasTower - I think you've basically cracked it. Now when I run the tests the _Anonymous Arrow Function_ is usually the fastest, the _Assigned Arrow Function_ the second fastest and the _Statement_ usually the slowest. It's still not clear to me why the statement should run more slowly than the two functions, but at least the wild discrepancy between one of the results and the other two is no longer there. If you want to do a copy-paste / write-up answer below I will accept - entirely up to you. Thanks, either way! – Rounin Dec 26 '22 at 17:27

1 Answers1

1

The anonymous function case is running faster because you've made a mistake in it: You're creating the function, but you don't ever run it. As a result, less code is being run, which is why it runs so much faster. To make for a more equal test, change the code to:

for (let i = 0; i < 10000; i++) {
  ((i) => paragraph.textContent = 'Loop iteration ' + (i + 1))(i);
}

With that change, the runtimes will be much closer, though still not equal. When i run it (and from your comment, when you run it), the statement code is still the slowest, by a small amount.

To speculate on why that is: Modern javascript engines (V8 for example) do a lot of stuff to optimize code behind the scenes, and one example is that if a function is run multiple times, it will try to rewrite your function to be more optimized. For example, the variable i could in principle be any type, so the compiled code that perfectly represents the javascript code would need typechecks and such so it can work with whatever you pass in. But once V8 has seen the function run a few times it can notice that i is always an integer, and it may create an version of the compiled code which is optimized to work with integers, but that's all it can do.

I speculate that it's better able to optimize your code when it's in a function form, than when it's written inline. But i would not rely on this: v8 is complicated and constantly improving/changing, so what might be the optimal javascript hack today might not be optimized in a couple years. Focus on writing code that's easy for us puny meat-humans to understand, and let the wizards of V8 handle the rest :)

Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • Thanks for spotting what I hadn't seen, @NicholasTower. And don't worry, I'm not engaging in _premature optimisation_. The issue above came up because I'm refactoring a dynamically-built function from `eval()` to `Function()` . I've read the latter is marginally more secure (since it may access only its own scope & global scope). I also wanted to reassure myself that it was faster. (It is, marginally.) When building my performance comparison I included a normal JS arrow function as a control and... was surprised to see it wasn't behaving as expected. I needed to get to the bottom of why not. – Rounin Dec 26 '22 at 19:41