I played with nim js and recorded the results here : https://www.grimoire-command.es/2022/nim_js_benchmark.html
It shows that nim js compiled JavaScript code is as fast as native JavaScript code for this particular example. Tested on nodejs and js78.
It also shows that nim e fib.nim took 6288,83s (and a --maxLoopIterationsVM:9223372036854775807 in fact switch("maxLoopIterationsVM", $high(int)) in a config.nims) . JavaScript versions on this setup were around ~30s.
I also played with memoization. nim e fib.nim runs in 0.40s then.
But for silly high ranks I ran accross something I reported here : https://github.com/nim-lang/Nim/issues/20853
For more serious benchmarks, don't use time command. Two hours :o It is better to use dedicated tools like https://github.com/treeform/benchy
Well, to be true I had to use uint64 instead of simple int to avoid an OverflowDefect exception For mathematical programs, we almost never use int. The bit sign matters.
It must be reminded that we do not compute fibonacci numbers this way. It is very inefficient. Even with memoïsation. Use matrix/polynomial fast exponentiation instead. This would make the benchmarks more interesting.
I am not sure these reveal anything about the languages. Maybe that Haskell do optimize really well such bad recursive code: https://github.com/drujensen/fib Fibonacci is a really well known example, and the one that serves as an example for compiler optimizers.
In my case I'm making a WebExtension, so code size is not my priority. But I would need a bleeding edge JavaScript WebExtension / browser API binding to actually use Nim at work.
I would also argue that the main modern websites are loading megabytes of JavaScript, even just to deal with advertisement monetization, so JavaScript code size is not a practical concern. We should avoid those megabytes, but in practice ressources are served gzipped and even ADSL connections are enough to keep page loads under 2s.
If it can help, JavaScript improved a lot since 1.5 and now supports a decent Python-like module / import feature (or built-in Sets for what I "recently" discovered).
Then, an other task I could have use Nim to, would be end-to-end testing via Selenium, but the Halonium project doesn't look active (nor finished -- declared lack of tests).
I still consider replacing my Makefile by NimScript tasks.
To finish, I gathered some JavaScript traps and would have loved to escape them thanks to Nim : https://www.grimoire-command.es/2022/javascript_traps.html
In my case I'm making a WebExtension, so code size is not my priority. But I would need a bleeding edge JavaScript WebExtension / browser API binding to actually use Nim at work.
I agree that the code size of Nim's output isn't so important: there are many tools to minify or compress JavaScript (closure, terser, etc). We wouldn't count the size of the C code generated against Nim unless it actually produced significantly larger binaries. So it is with JS.
Which browser APIs do you need? It is pretty straightforward to add bindings yourself. Here's an example of bindings to the browser's canvas API.
If I wanted to add more bindings of my own, I could just add them using the same method. For example that package doesn't have a binding for canvas' strokeText method. I could add it with:
proc strokeText*(context: CanvasRenderingContext, text: cstring, x, y: SomeNumber) {.importcpp.}
I agree that the code size of Nim's output isn't so important: there are many tools to minify or compress JavaScript (closure, terser, etc).
And the minified code is still much larger than the equivalent manually written code.
That is true, and for small programs there's not necessarily much gain.
I guess there's two ways of looking at a compiles-to-JS language: is it 1) a small syntax skin over JS maybe with compile-time checks (eg TypeScript, CoffeeScript), or 2) a significant semantic departure from JS (eg ClojureScript, Elm). I think you are evaluating Nim->JS as 1 when (in my opinion) it would be fairer to evaluate it as 2, or maybe 1.5.
Writing Nim->JS is a lot like writing TypeScript, but it has some advantages: you can write code that is Nim semantically not just syntactically, you can share code used for other Nim targets, and it gets away from many of JavaScript's "traps" (see Siltaar's link, the wat talk, etc). If Nim's JS output was closer to vanilla JS, the size of the generated code might be smaller, but some of the advantages would be lost.
The more valuable comparison to me is not Nim->JS vs vanillla JS, but Nim->JS vs JS + a utility libraries like lodash, RxJS, etc or other compiles-to-JS languages like Elm, ClojureScript, PureScript, etc. Each of those incurs a significant cost in terms of code size. But for complex programs (eg SPAs), the cost of those dependencies can be worth it.
A while ago I did some very unscientific comparisons and found that Nim->JS compared favorably in terms of bundle size in some cases. For example ClojureScript has immutable datastructures by default, and a lot of functions to work with them, so all that code gets included (Nim beat ClojureScript for size). In contrast, TypeScript does compile-time checks and a relatively small set of transformations (TypeScript beat Nim for size).
In other words, if you weigh a mouse, and a mouse plus a toolbelt, you should expect the mouse plus the toolbelt to weigh more, and if the mouse didn't need the toolbelt, that will seem like waste. But it's not a fair evaluation of the toolbelt itself.
Indeed, compiling the benchmark code with nim js -d:release -d:danger and then minifying with esbuild --minify produces this output:
var framePtr=null,excHandler=0,lastJSError=null;Math.trunc||(Math.trunc=function(r){return r=+r,isFinite(r)?r-r%1||(r<0?-0:r===0?r:0):r});function isPrime_452984833(r){var e=!1;r:do{var a=2;e:do{a:for(;;){if(!(a<r))break a;if(Math.trunc(r%a)==0){e=!1;break r}a+=1}}while(!1);e=1<r;break r}while(!1);return e}var i_452984841=[1],found_452984842=[0];r:do{e:for(;;){if(!(found_452984842[0]<3333))break e;isPrime_452984833(i_452984841[0])&&(found_452984842[0]+=1),i_452984841[0]+=1}}while(!1);
Which is only 490 bytes, slightly larger than haxe (361B) but smaller than the rest (except JS). Not very fair though as the other languages presumably weren't minified.
Interestingly the output above runs faster than the reference JS on my machine (168ms vs 179ms)
This JS trap could be actually mathematically correct, if we understand max and min like sup and inf:
>> Math.max() > Math.min()
false
>> Math.min()
Infinity
>> Math.max()
-Infinity
See: https://math.stackexchange.com/questions/4081838/minimum-of-empty-set-in-infty-why-is-that