Are JavaScript for loops better than filter() and forEach?()

Over the last few years, the methods in ES6 like filter(), forEach() and map() have been ever so popular. But are they actually better?

By Aman Bhimani

November 15th, 2020

We are all used to the clean code that Array.map and Array.filter provide us, but for your real world applications, are regular for loops better than these convenient methods?

There are a few metrics we will consider when comparing the methods in the Array class, to the “old” for-loop way of doing things: performance, readability, and scalability. Here is one example of the types of loops we will be comparing today:

const items = [’abc’, ’def’, ’ghi’, ’jk’];

This is a perfect use case for the filter method:

const threes = items.filter(item => item.length === 3);

Or the for-loop way of solving the problem:

const threes = [];
for(let i = 0; i < items.length; i++) {
  if (items[i].length === 3) {
    items.push(items[i]);
  }
}

In Search For The Best

With all of the methods that we are considering in this article, it is clear that their time complexity is O(n). Meaning that at the worst case, we have to at least every single element in the array to solve the problem. Just think about it: we cannot filter out items that are length 3, without actually looking at every single item. If we stop in the middle, which would be O(n/2), we have not solved the problem.

Considering we have the same time complexity, we need to consider three questions:

  • Which method is faster?

  • Which method is more scalable?

  • Which method is more readable and maintainable?

Factor One: Performance

Performance can be calculated in JavaScript with the metric of operations per second. Though we cannot access the CPU directly to test the performance of our code so close to the “metal”, what we can do is benchmark how fast our code runs.

We will make use of a library called Benchmark.js to help us in this, which will give us statistically significant results. Benchmark will run our code for several iterations, and average the results out for us. For the test, I have chosen an array of 1000 “Person” objects which have properties like: name, age, and gender. For our benchmark test, we will filter for any person with age more than 10.

const people = [
  {name: ’Jane Doe’, age: 32, gender: ”Female”},
  ... 999 other persons
}

Screenshot by the Author, Aman Bhimani. Library: Benchmark.js.Screenshot by the Author, Aman Bhimani. Library: Benchmark.js.

To our surprise, for-loops are much faster than the Array.filter method. To be precise, the Filter method is 77% slower than for loop. Why is this? One reason could be that for-loops run synchronously, and the filter method is creating 1 new function for each element in the array. This new function is then put on the call stack, and run one by one by the event loop. This overhead takes time, which the for-loop completely omits, and runs your operations right there in the same function.

It is clear that the for-loop is the clear winner for pure performance if we look at the metric of operations per second, or time. We will have to see how it stands in the rest of the two metrics, and if it is a clear winner or not.

Factor Two: Readability

Of course, one of the main factors of writing code is readability. More readable code is more maintainable, even if it is just a simple for loop. When we look at words rather than numbers in a for loop, we understand it. Array comes with method names which are much more descriptive than writing a for loop.

  • indexOf = finds the index of the first element that matches

  • forEach = runs the function for each element

  • filter = filters for any elements that match

    const threes = []; for(let i = 0; i < items.length; i++) { if (items[i].length === 3) { items.push(items[i]); } }

Let’s take our earlier example of threes and analyze it a bit more.

  • The first line is meaningless, we create a new array to hold our filtered elements, but it does not add any business logic.

  • The second line is also meaningless — it is meant to just loop through the entire array, no business logic here.

  • The third line is where the logic is, we need elements that are length of 3.

  • The fourth line has no logic either — it just finishes off our function.

    const threes = items.filter(item => item.length === 3);

When we compare this to our filter example, it is confined to a single line, which has all the logic needed.

Factor Three: Scalability

In order to understand the scalability in JavaScript, we need to first understand how JavaScript works at the core: the event loop and event queue. Since JavaScript is single-threaded, we only have 1 operation running at a time. Modern websites and web apps need to do multiple operations at a time, so how does that work? The event-loop makes this possible by sharing the single thread which is available to the browser’s window.

Sketch by the author, Aman Bhimani.Sketch by the author, Aman Bhimani.

The diagram above describes how the JavaScript event loop works. We have the event queue, which stages events in a First In First Out queue. When a function is started in the event loop, it creates its own “call stack” which is where all the function references go in a Last In First Out stack. But the question is, does this really help us in the loops?

Unfortunately, the for-loops or forEach do not use the event-loop, but they are rather synchronously run (for-loops) or put on the call stack (forEach). This also means that using either of these ways of looping through data will block any other task that needs to run on the main thread.

As an example, we can consider the following code. On the loading the body, we start a function which prints the first 10,000 numbers. And we have a button which runs a function that prints “HELLO”.

function start() {
  for(let i = 0; i < 10000; i++) {
    console.log(i);
  }
}

function print() {
  console.log('HELLO');
}

index.html:

<body onload="start()">
  <button onclick="print()">print</button>
</body>

When the page loads, the printing of numbers starts. If we press the button before* ***number 9999, it won’t be printed immediately. This is because the button onclick event, was put on the event queue. It will run as soon as the event loop is free to call on the next event. It does not matter which type of loop we use to iterate.

1
2
————-> click button
3
...
9994
9995
9998
————-> click button
9999
HELLO
HELLO

How Do We Make This Better?

If your array size is “small”, then it’s obviously better for the runtime performance to use regular old for loops. However, if you are looking for the most scalability, we need to think better. We need to abuse the language mechanics, and make use of the event-loop. There are a few ways you can go from here:

  • Each iteration is a new event

  • Each “chunk” of the array is a new event

In our next practice, we will try and make a “for” loop which takes the event loop into consideration and makes this for loop idea infinitely more scalable for larger sets of arrays.



Continue Learning