[Photograph of Neil Fraser]

Neil Fraser

For the past decade I've been a software engineer at Google in California.  In my spare time I've created a mélange of open-source software.  I've also built some rather unusual hardware.  On rare occasions I've even been known to write something interesting.

JavaScript Loops Monday, 7 June 2021

As JavaScript interpreters get more sophisticated, their performance behaviours get more complex and more challenging to understand. Let's take the following code which counts to 100 million:

console.time('100M test');
for (var i = 0; i < 100000000; i++) {
  if (i === null) alert();
}
console.timeEnd('100M test');

How you run this code make a huge difference.

Chrome 91 Firefox 89 Safari 14
Pasted into console 284 ms 40,800 ms 256 ms
Run in a webpage in the global scope 310 ms 640 ms 256 ms
Run in a strict or non-strict function scope cold: 300 ms
warm: 32 ms
60 ms cold: 256 ms
warm: 85 ms
Run in a non-strict function scope using eval 35,173 ms 2,451 ms 35,690 ms
Run in a strict function scope using eval 300 ms 1,100 ms 18,077 ms

These tests reveal several important facts when dealing with performant code.

  1. Never write code at the global scope. Create a function, then call it (or use an Immediately Invoked Function Expression (IIFE)).
  2. Never paste code straight into the console when measuring performance. Create a function, then call it (or use an IIFE), all of which may be done from the console.
  3. Never define a helper function inside a function, since it will start cold every time.
  4. Never use eval, especially in non-strict mode. For user-defined dynamic code, use (new Function(...))(); instead of eval(...);

With this out of the way, here's a simple tool that wraps the user-provided code inside a function, runs it five times (to allow the function to 'warm up'), and prints the results to the browser console.

Chrome 91: 290 - 32 ms
Firefox 89: 60 ms
Safari 14: 256 - 85 ms

Now that we know the environment in which to run performant code, let's look at the best ways to loop in JavaScript. First let's try a simple "for" loop.

Chrome 91: 500 ms
Firefox 89: 150 ms
Safari 14: 160 ms

One optimization is to cache the length. This has a noticeable speed up in Chrome, at the expense of a small penalty in Firefox.

Chrome 91: 440 ms
Firefox 89: 160 ms
Safari 14: 160 ms

ES6 adds the "for-of" loop. It's just a performance disaster in every browser. Avoid this construct for performant code.

Chrome 91: 540 ms
Firefox 89: 930 ms
Safari 14: 262 ms

Likewise, the technique of looping through non-falsy values without checking length is not advisable.

Chrome 91: 540 ms
Firefox 89: 508 ms
Safari 14: 190 ms

Not surprisingly, "while" loops are the same as "for" loops. Just more verbose.

Chrome 91: 440 ms
Firefox 89: 160 ms
Safari 14: 160 ms

The performance of unrolled loops is wild. Horrible initial penalty when cold, then dramatic warmup after one or two executions.

Chrome 91: 1,600 - 440 ms
Firefox 89: 818 - 0 ms
Safari 14: 2,570 - 120 ms

Arrays are not the only thing one loops over. NodeLists are unusual in that they are generators. This changes the performance curves. A regular for loop (with cached length) is much slower on a NodeList than on an array.

Chrome 91: 4,650 ms
Firefox 89: 3,800 ms
Safari 14: 2,740 ms

The non-falsy value technique which used to be recommended by the Google JS style guide is now slower than a regular loop with cached length.

Chrome 91: 4,750 ms
Firefox 89: 3,900 ms
Safari 14: 2,900 ms

Since NodeLists are generator, maybe a "for-of" loop would be faster? Absolutely the opposite.

Chrome 91: 23,500 ms
Firefox 89: 9,300 ms
Safari 14: 5,000 ms

So to summarize, when writing performant JS loops:

This research is accurate as of June 2021. Edge 91 performs identically on all tests as Chrome 91. Sorry, I can't run MSIE on my Mac. Let me know if you have any thoughts.


Older news:   Lego Storage  -  Five Years  -  JavaScript Chain Reaction  -  Play Well  -  Archive

-------------------------------------
Statistics Private Exit