What if I told you everything you knew was a lie, what will happen if you learn some of the key features our beloved ECMAScript have published over the recent years, are actually dangerous performance traps, sugar coated in a slick looking one line callback functional code? This story starts a few years ago, back in the naive days of ES5…
I still remember this day vividly, ES5 was released, and great new array functions were introduced to our dear JavaScript. Among them were forEach, reduce, map, filter — they made us feel the language is growing, getting more functional, writing code became more fun and smooth, and the result was easier to read and understand.
About the same time, a new environment grew — Node.js, it gave us the ability to have a smooth transition from front-end to back-end while truly redefining full stack development.
Nowadays, Node.js, using the latest ECMAScript over V8, is trying to be considered as part of the major league server-side development languages, and as such, it needs to prove worthy in performance. Yes, there are so many parameters to be taken into account, and yes, there is no silver bullet language which is superior to all. But, is writing JavaScript using the out-of-the-box features provided like the mentioned above array function helping or harming your application performance?
Moreover, client-side javascript is claiming to be a reasonable solution for more than just presentation\view, as end-users computers grow stronger, and networks faster — but can we rely on this when our application requires blazing fast performance and might be a very large and complex one?
To test these questions, I tried comparing a few scenarios and drilled down to understand the results I got. I executed the following tests on Node.js v10.11.0 and in the Chrome browser, both on macOS.
The first scenario which came to mind was summing an array of 10k items, this is a valid real-life solution I stumbled upon while trying to fetch a long table of items from the database and enhance it with the total sum, without having an additional query to the DB.
I compared the summing of random 10k items using for, for-of, while, forEach, and reduce. Running the tests 10,000 times returned the following results:
For Loop, average loop time: ~10 microsecondsFor-Of, average loop time: ~110 microsecondsForEach, average loop time: ~77 microsecondsWhile, average loop time: ~11 microsecondsReduce, average loop time: ~113 microseconds
While googling how to sum an array, reduce was the best-offered solution but it’s the slowest. My go-to forEach wasn’t much better. Even the newest for-of (ES6) provides inferior performance. It turns out, the good old for loop (and also while) provides the best performance by far — 10x better!
How can the newest and recommended solution make JavaScript so much slower? The cause of this pain comes from two main reasons, reduce and forEach requires a call back function to be executed which is called recursively and bloats the stack, and additional operation and verification which are made over the executed code (described here).
While this sounds like a less interesting scenario, this is the pillar of immutable functions, which doesn’t modify the input when generating an output.
Performance testing findings here again show the same interesting trend — when duplicating 10k arrays of 10k random items, it is faster to use the old school solutions. Again the trendiest ES6 spread operation `[…arr]` and Array from `Array.from(arr)` plus the ES5 map `arr.map(x => x)` are inferior to the veteran slice `arr.slice()` and concatenate `[].concat(arr)`.
Duplicate using Slice, average: ~367 microsecondsDuplicate using Map, average: ~469 microsecondsDuplicate using Spread, average: ~512 microsecondsDuplicate using Conct, average: ~366 microsecondsDuplicate using Array From, average: ~1,436 microsecondsDuplicate manually, average: ~412 microseconds
Another frequent scenario is iterating over objects, this is mainly necessary when we try to traverse JSON’s and objects, and while not looking for a specific key value. Again there are the veteran solutions like the for-in `for(let key in obj)`, or the later `Object.keys(obj)` (presented in es6) and `Object.entries(obj)` (from ES8) which returns both key and value.
Performance analysis of 10k objects iterations, each of which contains 1,000 random keys and values, using the above methods, reveals the following.
Object iterate For-In, average: ~240 microsecondsObject iterate Keys For Each, average: ~294 microsecondsObject iterate Entries For-Of, average: ~535 microseconds
The cause is the creating of the enumerable array of values in the two later solutions, instead of traversing the object directly without the keys array. But the bottom line result is still causing concerns.
My conclusion is clear — if blazing fast performance is key for your application, or if your servers require to handle some load — using the coolest, more readable, cleaner options will blow a major punch to your application performance — which can get up to 10 times slower!
Next time, before blindly adopting the slickest new trends, make sure they also align with your requirements — for a small application, writing fast and a more readable code is perfect — but for stressed servers and huge client-side applications, this might not be the best practice.