Hacker News new | comments | ask | show | jobs | submit login
JavaScript and Node performance coding tips to make applications faster (voidcanvas.com)
109 points by osopanda 10 months ago | hide | past | web | favorite | 64 comments

The quality of this article is readily apparent in the first recommendation.

Global variables are slow... because of having to walk scope chains? Uh, no. Scopes are static properties (well, once the with statement was eliminated--now you know why that doesn't exist anymore): the compiler only has to look up the scope of the variable during compilation, which means it carries no penalty by the time you get to a JIT. The reason why global variable lookups can be slow is that global variables are often properties on objects with non-trivial access costs (such as a DOM window, if you running on a webpage), as opposed to an inaccessible lexical environment where all references are statically known.

(It's also somewhat disingenuous that all of the coding tips are about keeping v8 happy, as if v8 were the only JS JIT that mattered or v8 characteristics were somehow universal in JS JITs, neither of which is the case).

Running through the some of other bits:

* I'm not sure homogeneous arrays is an optimization benefit for other JITs than v8.

* Actually, traditionally JIT compilers focus on hot loop nests as well as functions. The original TraceMonkey JIT (first JS JIT) considered loops a starting traces, for example.

* Changing shape of objects does matter for monomorphic calls. Although not all JITs are as insistent on monomorphism as v8.

* True, but isn't this obvious?

* Last I checked (admittedly quite a while ago), JITs tended to optimize based on shape, not class. So there's no difference in performance between the two cases.

* Or you could use for/of. Or you could just never use sparse arrays because no JIT likes having these holes. I thought you were giving performance tips, it seems weird not to mention one of the more universal ones here.

* Try/catch is generally free if you don't throw an exception.

I could go on, but it's not worth it at this point...

The try/catch wasn't always that case. I'm having trouble finding the blog from one of the v8 developers who went into details about it and other optimizations. It was on HN a while back.

Here is a perf test with various v8 versions (as you noted not only JIT but going going with the authors favorite).


My recollection is that the nonperformance of try/catch is something specific to the v8 JIT. Historically, the zero-cost exception model of try/catch has you do absolutely nothing with the data until someone throws an exception, at which point you work out how to map the return address to the appropriate catch handler.

v8 used to not be able to optimize blocks containing try/catch, which is why if you needed it, you relegated it to a function containing just the try/catch block.

but since async/await relies on the use of try/catch, last november's (october?) v8 release can optimize try/catch.

plus, it was only ever a v8 issue, not an issue in javascriptcore nor tracemonkey.

>The try/catch is a very costly operation. You should write your code in such a way that exceptions are not uncertain.

I believe all major JavaScript engines now optimize try/catch.

And SharedArrayBuffer is now disabled by default due to Spectre. I don't see it coming back soon either.

Also, I dislike this article because it recommends these things "always". "Always use forEach", "always avoid delete", "always...". Not to mention it's yet another v8 specific set of optimizations.

Yep, try/catch is optimized since node v8.

The biggest takeaway from the latest batch of optimizations I read about is this:

Write idiomatic JavaScript code!


Just because you're working with a dynamic language doesn't mean you should abuse it. Don't write functions with weirdly changing parameter arities, or dynamically change the types of variables.


These things are all hints and micro-optimizations by themselves. Don't prematurely optimize at the cost of readability and maintenance. Profile and only fix things that need fixing!

Also, I dislike this article because it recommends these things "always". "Always use forEach", "always avoid delete", "always...".

Look on the bright side: at least the article isn't titled "best practices"...

Or "curated".

Try/catch point is an in-general thing. Irrespective of language. Always better to handle errors by yourself.

I was not aware of the browser deprecation of Shared array buffer. But this (or something like this) is a part of the ECMA proposal which is in stage 4. https://github.com/tc39/ecmascript_sharedmem

I said using forEach (or in-builts) are always a good practice. Now like you have to. For an example, if you have an array with no holes in it, for is better performant then forEach.

I find "for of" to be much more legible than "forEach". Not much difference in general but the "for" the beginning immediately tells me that there is a loop here, whereas I have to read more thoroughly to identify something as a loop in the forEach case.

  // for ... of
  for(let node of nodes){

  // forEach
  nodes.forEach(node => {

For...of is fantastic and I use it quite often (especially in async/await codebases as it doesn't create a new function context!)

However it is slow currently in all major browser engines. For fast-path code, you should reach for something else (like a for(;;) loop), but for most other loops I always tend to reach for for...of for it's ease of use and compatibility with iterators makes it a pleasure to use!

Are you sure about that? "for of" is something that I'd expect a js compiler to handle exactly the same way as forEach. I'd see how it might have been slower right when it came out but browser vendors are quick in optimizing their compilers.

At least https://jsperf.com/for-vs-foreach/293 shows that there is no significant difference in for of vs forEach in my version of chrome. There is an insignificant difference acoording to which forEach is actually slower. (One forEach case in that benchmark is only faster because it does something different.)

Edit: looking at the comparison at the bottom, it seems like forEach is actually significant slower than for of since chrome 61.

Lots of work went into making the perf of for(of) fast - after the initial “make it semantically correct” bits.

There are many reasons it is faster, but it also has the nice property of working sensibly on any iterable object rather than forEach that only iterates by index, and has an annoying ‘if (index in object)’ in the loop.

for of has got a lot faster recently but it's still never going to beat the performance of a normal for loop due to the semantics - each turn of the for of loop requires a new iterator object to be allocated.

Ya know what I'm not as sure as i thought I was. I'm really glad that for...of isn't nearly as slow as it was when it was first profiled it.

Thanks for that!

the forEach is running a function on each node, passing the node as a parameter and console.logging that node.

the for of is a loop.

at any rate it seems the kind of difference in legibility that would vanish with very little familiarity, I would expect for most people developing JavaScript regularly that forEach would be more legible - even with the arrow function - because it is the construct they encounter and use more often day to day.

> For an example, if you have an array with no holes in it, for is better performant then forEach.

Do you mean it's faster? (Don't use performant: use fast or faster instead.)

I might be wrong, but 'faster' strikes me as a more ambiguous word. In this context it would be clear, but I can see how consistently using performant would help in clarifying what this 'faster' you're talking about is.

>Always better to handle errors by yourself.

I disagree, using try/catch/finally for their intended purpose (exceptional cases) is going to be preferable to trying to replicate their function on your own. Even if just from a semantic standpoint (is semantic the word i'm looking for here? it feels wrong...).

>But this (or something like this) is a part of the ECMA proposal which is in stage 4.

I really dislike the idea of true shared memory for the extreme vast majority of cases, and i'm not particularly happy about it's inclusion into browsers, so take what I say here with an appropriate amount of salt. I personally think spectre is only the first of many many problems it will bring if fully integrated. Transferable objects are a much cleaner and safer pattern to use in most cases, and they are here today in all major browsers. I do admit that having shared memory available is a massive bonus to some kinds of problems, but I feel the net impact will be negative. That being said, until a fix for spectre is either in hardware, or they find a way to solve it in software that doesn't destroy SharedArrayBuffer's performance, it's not coming back. And I don't see those happening within a few weeks/months in all honesty.

>I said using forEach (or in-builts) are always a good practice.

That was only one example of many in the article that I felt was making advice which works on one engine (v8), in a well-defined set of situations (sparse array, hot code paths, no async/await in the case of forEach), into a general truth. Not many things are universally true, and spelling out the when, why, and where you should apply this advice will go a long way toward teaching others rather than just giving people a list that they can haphazardly apply.

Like your advice to avoid for...in. If you are iterating over enumerable properties, there is no reason to avoid it. It's slow, because that operation is slow, and most attempts to do it yourself more quickly will either fail, or will have edge cases that may introduce bugs. It's good advice in hot-path code, but shouldn't be universally followed. Especially when the alternative to a few lines of well-understood code is a mess of if statements, key lookups, and undefined/null checks.

In Python try..except is very idiomatic.

I wish JavaScript had Python's exception handling. There's no real way in JavaScript to catch only exceptions you're expecting, other than checking the error manually and re-throwing if it doesn't match.

This is a very mediocre article that mixes some good advice with misleading and frankly bad things.

Justifying forEach (which is several times slower than a for loop) with sparse arrays (which are an anti-pattern because they take you out of "fast elements" mode in V8 at least) is laughable in a sad way. It also has no "break" functionality , which is important if we are talking about performance (i.e. when iterating the array to find a specific member).

The advice for using array literals to insert elements is also bad for the similar reason that it makes it easy to create sparse arrays.

That and "i>arr.length" in the example means the for loop will run exactly 0 times! ;)

Using "filter", "map" and "reduce", by the way, is also slower than using a for loop, even if broken out into a separate function for purposes of chaining. This is because they call a function on each iteration and function calls (even with optimisation) are inherently more expensive. So, use them only when this difference in performance does not matter.

The closure / timer example is just so convoluted. Of course the closure will keep "bar" in scope - that's what closures do! The "foo" object doesn't somehow magically own "bar" it just has a reference to it, same as the closure. If something still references an object then GC will not touch it.

Similarly for the event listeners advice, which is also badly worded.

And if you do need to write a timeout-based loop, I respectfully suggest the following construct:

  const interval = 1000;
  var timer;
  (function loop () {
      ... do stuff ...
      timer = setTimeout(loop, interval);
Now you can even use "rewire" to control the interval during your unit tests - bonus!

The Arrays vs Objects thing is just shallow. In reality, the advice is "it depends". If you need to iterate, use an Array. If you need to access by key, use an Object (or better a Map). If you need both (and this frequently happens in my experience) then you have to decide depending on the size of your structure.

Thank you, you saved me the hassle of writing a very annoyed rant.

Here's a performance tip: learn how to measure your damn code in the context of the website, because the JIT depends on heuristics that involve the context of the whole code.

Open website -> Open Dev Tools -> Performance -> Do Stuff for a reasonable amount of time.

Then you can actually reason about what causes the bottleneck based on the numbers. If you're on Chrome, you can even look at the source code after using the performance tab for a while, and see a line-by-line breakdown of where most of the time was spent, and find the hot path down to the individual expression or statement. Fix real bottlenecks, not hypothetical ones.

> It also has no "break" functionality , which is important if we are talking about performance (i.e. when iterating the array to find a specific member).

The article is woefully misinformed regarding `forEach` and I agree that the array methods in general are slower [1], but `some` will bail out on the first true value returned. To be sure:

    [1,2,3,4,5].some(function(n) { console.log(n); return n>=2; });
will not run the callback function after processing the 2

[1] https://news.ycombinator.com/item?id=7938173

"some" !== "forEach" ;)

Also, "some" still has the performance penalty of an extra function call per iteration (as I know you know based on your linked comment) and we are talking about performance advice here.

But good point - I've never actually used "some" before, so I learn something new today - thanks :)

the latest versions of v8 (and others) have optimized the paths for calling these array functions and likely break down into for/while loops when they're jitted.

While the points are correct, they are bad advice, except for the last point: prefer O(1) over O(n) because all (premature) optimizations are evil! What works in one engine for example (Firefox) might not work in Chrome, or the next version of the same engine. So write stupid simple naive code until you actually need to optimize. Then go for algorithms first, changing something from O(n) to O(1) will have a bigger impact then for example creating static binaries (which most of the time will be slower because there will be no run-time optimizations)

The original premature optimization quote was specifically about spending a lot of time optimizing complicated algorithms in an environment in which computation was often business required if used so as a consequence one could count on people staying around more than insignificant amounts of time to get a response.

In the environments most of this would be used on people do not stick around long periods of time, therefore any simple optimization that one can do in the preliminary development of an application is not premature.

But this article sucks, as do most articles with a list of coding tips for a particular language/platform.

I don't find it "premature" optimization at all. IMHO it is barely "optimization" if you're just using the right tool for the job (namely a hash table instead of a list when you need key-value lookup).

I'm confused about the setTimeout example; the code as presented is creating an infinite loop, so it does not surprising that the object used in the loop is not garbage collected. But I would expect references inside a timer to be collected once the timer has expired. Do they not ?

> Avoid O(n), try O(1)

Be careful with this, as dictionaries are not really O(1), in the worst case, it can be actually O(n). When your code heavily evolves around the 'fact' that it's O(1) try to make a version with a simple array iteration and compare which is faster.

I believe a good rule of thumb is that looping over an array will generally be faster than using a hash map until you have around ~1,000 items.

Very approximate and dependent on the case obviously, but I always find it surprising how far you can go with just a simple array.

From experience I would say it's closer to 100 items, though this is using Go as benchmark. 100 items is also what I would expect considering CPU caches.

From 100 to 1000 items the array performance is not massively worse and shouldn't matter much but a dictionary can beat it (by a tiny bit).

If you're over 1000 definitely use a dictionary or tree.

This article is quite incorrect. Some of these points were valid many versions ago (try/catch did have a cost with it several V8 majors ago, but that has since been resolved).

And, again, for the last time, Node.js is not actually single threaded and may whatever deity help you if you choose to start additional threads without understanding how libuv works. This isn't java, the answer isn't to throw more threads at it.

Honestly, it's this kind of stuff that really hinders javascript. So many people write "expert" articles that show fundamental misunderstandings about the core of JS (including setTimeout vs setImmediate!)

> Closure & timer – a deadly combo

In practice you can avoid this problem if your function wrapping a timer is not a method and the callback of the timer is a recursive call to the very function wrapping the timer. In that case there is no object to access to get to the timer's function and secondly the this function never nullifies or removes from garbage collection until the last timer is called.

> setImmediate over setTimeout(fn,0)

Yes, agreed, but I wouldn't ever run timers in a loop to achieve parallel asynchronicity. I would use native Node methods to achieve this. I would however use timers to achieve sequential asynchronicity, also known as polling, which is where setTimeout would be beneficial. Sequential operations likely to always be slower than parallel operations, but sometimes you only just need intermittent delays.

I agree that these recommendations in the article are really good ideas.

Any team with enough developers using Node should develop a handful who can understand the v8 internals and help troubleshoot real-world performance problems.

The most helpful resource I've found about v8 specific performance is the bluebird optimization killers wiki: https://github.com/petkaantonov/bluebird/wiki/Optimization-k... wiki, and the v8 bailout reasons repo: https://github.com/vhf/v8-bailout-reasons

The best advice I can give about performance engineering is "all performance guidelines are bullshit." Compilers and JITs are constantly improving, which means any specific guideline like "do X to avoid a JIT bailout" isn't necessarily true going forward. Furthermore, the benchmarks that compiler writers use to assess the utility of their optimizations aren't hand-optimized unreadable kludges, it's generally typical applications.

Write readable, natural code, and compilers are more likely to eventually optimize it, even if perhaps they do a bad job today. Only contort your code to improve performance if you have profiles showing that you need to do so, and even then, see if there's natural code that works first.

Do you think adding type annotations or using typed js (typescript/flow), the optimisations could be improved? Since the types are providing more metadata about the code?

No (with an asterisk). The state-of-the-art for JS optimization already computes shape information (roughly equivalent to typing) and feeds that into optimizations.

The asterisk comes from the costs of having multiple shapes for a given variable. V8 (to my recollection, it could very well be out of date) generally has a performance cliff between monomorphic and polymorphic functions: if the added strictures of typechecking is giving you monomorphic functions, it could see a performance improvement. Other JITs (again, to my recollection) are generally happier to have polymorphic functions where the degree of divergence is still small (say, two or three shapes), although having 1000 shapes is going to be unhappy for everybody.

Note that this discussion is effectively microoptimization-level discussion: don't rearchitect your code to enforce monomorphism at the expense of clarity unless you have profiling evidence that the performance is necessary.

Except Crankshaft is replaced with Turbofan in Node.js 8+ so a lot of these no longer apply.

(I'm the author of this last repo.)

You are correct that these no longer apply to the most recent Node versions. Crankshaft is still being used in Node <8.3.0.

Crankshaft got totally disabled starting from V8 5.9. Node 8.2.1 uses V8, Node 8.3.0 uses V8

I'm pretty sure there is still a big number of projects running on Node <= 8.3.0.

I'll make it clear in this project README though, whenever I find time. :)

True, except if you track LTS (probably not a bad strategy) and entirely skip the earlier 8.x versions ;)

> Avoid try/catch

haha, async await needs that by design!

It's not required, you can use .catch since it's just normal promises:

const result = await doStuff().catch(errorHandler);

So what do you think your result variable contains in the event that the promise is rejected? Is this semantically equivalent to try/catch?

(async () => { const result = await Promise.reject(1).catch(e => e); return result; })()

// =>Promise {<resolved>: 1}

As you can see, the variable contains a promise returned by the .catch. It's not equivalent to try/catch (in that case the variable won't be defined for one thing), but it does allow for error handling without using try/catch if one is inclined to do so.

In practice, I've been using async/await extensively in Node.js/Express, and I'm yet to write a single try/catch. In Express, I simply use a wrapper function that catches promise rejections and siphons them to error handling middleware:

const asyncIt = fn => (req, res, next, ...args) => fn(req, res, next, ...args).catch(next);

These tips are all great. Thanks for the article.

My 2 cents from working over 5 years with NodeJS is to be careful when you sacrifice code readability over performance.

`.forEach`, `.map`, `.filter` are way more readable / maintainable than a 20 lines `for loop` with n+ var assignments.

As for `try..catch`, I use ( readability - again ) to follow a Promise chain.

    return new Promise( resolve => resolve( JSON.parse( req.body ) ) )
                  .then( body => db.update( body ) )
                  .catch( err => /* will catch JSON.parse as well as db.update */ )

If you want to squeeze more performance out of JavaScript, at some moment you will have to deal with the gatekeepers of performance: inline caching, hidden classes, deoptimizations, garbage collection.

I feel like these "N performance tips" make it onto the main page mostly because of the commenters coming up with counterpoints to each "tip".

Real performance tips are always in comments!

My favorite HN articles are blindly miseducated posts with comments explaining why the post is wrong or misleading. Sometimes more informative than well written posts...

Promise.all vs sequential awaits is a good tip but Only if the results of the awaits are independent (obvious). I see that all the time. It’s easier to see how inefficient that is when you’re chaining .then().. await hides that and gives the impression that it’s parallel.

Second point is, global variables are not cleared by the garbage collector. So if you continuously add more and more global variables (which are not of future use), it will cause a memory leak.


It's an easy thing to do in JS, it happens if you forget the `var`.

only when not in strict mode, which is becoming more and more rare, especially in javascript codebases (as opposed to one-off scripts)

I think this is true because they hold a reference in window.

Sure that will prevent gc, but that doesn't mean it's a memory leak

It is if the variable is accidentally in global scope due to hoisting if it is not explicitly defined in a finer scope. Not an issue in strict mode, you'll get a reference error instead of an implied global.

If you're never going to use it again, and you're never going to free it, it's a memory leak.

No, it is not. A memory leak has to grow. Without the leak part, it is just ...allocated memory.

What if I specifid the `global` scope, like `global.SOME_VAR`? Will that skip the expensive search of the parent nodes?

It cannot. It still has to check whether there is a local variable in any other scope that is above the one currently being executed, all the way to the top where it finds global.

Same with window in a browser context. You could still have a "window" variable placed between your execution context and the global context.

"Create class for similar kind of objects" would using ECMAScript 2015 classes be acceptable?

Profiling might be a useful addition to the list,

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact