Very cool project! I think JS is very underrated for quality games.
Another "easy" optimization opportunity: don't use .forEach() unless necessary. I noticed it was used in the benchmark.
.forEach() has the overhead of a function call on every iteration, you can't break out of the loop early if needed, and the scoping is different depending on whether or not you use a classical function or an arrow function.
Given that a rewrite in C/C++ was not feasible but a substantial optimization was acceptable, would solutions like AssemblyScript have been a good strategy? In the end the answer will greatly depend on the amount and nature of dynamic allocations though.
Simple but annoying and error-prone optimizations are not necessarily easy ;-) Alternatively, the author might have been not confident enough about C/C++ that a rewrite is straightforward (a hypothesis from the fact that one's Github profile doesn't have any C/C++ code).
Reusing a couple objects and packing numbers are all things you'd do in a systems language, too, except you'd have a thousand more issues to deal with.
You can definitely get a lot out of JS if you start trying to "write it low-level", but it also definitely becomes a case of fighting the language to do so, because everything that it tries to help you with becomes a sharp edge for performance.
So, my experience with doing this kind of thing(and I did plenty of stuff with Actionscript back when) is that it's actually helpful to write your own codegen, even if you're still targeting the high level environment as output, because then you can define a syntax that clarifies the kind of language butchery you're doing, which in nearly every instance has something to do with allocation. Everything that needs to be fast can be programmed towards a preallocated chunk of integers, and cursors that operate over them. When you need to step outside of that and do something a bit higher level, you just bolt on idiomatic JS code and, you know, reuse objects in your algorithms by doing some load and store ops on them, or making them relative to a stack or a pool or whatever. It doesn't have to be an all-or-nothing thing.
This doesn't slow down game development very much, because, while you are adding a bunch of non-idiomatic, less debuggable stuff, again, your bottleneck is almost always about allocation and stuff that just needs to be a fast "array of whatever." Sometimes you iterate over the array, sometimes you indirect or you have a temporary buffer. But it's something that you can plan to have not be totally perfect and still address a hotspot where you would be allocating thousands of times a frame otherwise and thrashing the GC.
All these performance problems people have, that shouldn't exist in the first place if they properly programmed things right from the start. Not their fault, though, because the ones who teach are the ones who did it wrong before them.
Another "easy" optimization opportunity: don't use .forEach() unless necessary. I noticed it was used in the benchmark.
.forEach() has the overhead of a function call on every iteration, you can't break out of the loop early if needed, and the scoping is different depending on whether or not you use a classical function or an arrow function.