cljs-shrinkwrap bundles up your nbb script, plus nbb, plus deps from node_modules into a single "binary" which only depends on Node.js:
npm i cljs-shrinkwrap
npx shrinkwrap myscript.cljs myscript
This will produce an executable script in ./myscript that only needs Node to run.
https://github.com/chr15m/cljs-shrinkwrap
I'm not sure if this deserves to be a utility since it's basically just following the steps from the nbb wiki.
One thing I could do is fold this into nbb itself, similar to nbb bundle - something like nbb shrinkwrap which would depend on having @vercel/ncc installed. 🤔 Would that make more sense than a standalone utility?how is ncc different from esbuild etc?
Hm good question. I think I tried esbuild and found the configuration too complicated whereas ncc is one command to do the exact thing I wanted. That said it seems like ncc might have issues supporting esm.
I replaced ncc with esbuild and it works well, however when bundling a small script that was working with ncc I am now getting an error. Working through it.
Ok the error was to do with a dynamic require of node builtin "events" not being handled/translated by esbuild. The LLM gods suggested:
:banner {:js "import { createRequire } from 'module';const require = createRequire(import.meta.url);"}
And this magical incantation fixes the issue. 🕉️
So if we added a nbb shrinkwrap command it could be dependent on esbuild instead of ncc. Instead of making esbuild a requirement we could check for it at runtime and dynamically import it and print a useful error if it isn't present.
(p/let [esbuild-result (esbuild/build
(clj->js {:entryPoints [(fs/realpathSync mjs-file)]
:bundle true
:platform "node"
:minify true
:format "esm"
:banner {:js "import { createRequire } from 'module';const require = createRequire(import.meta.url);"}
:write false}))
output-file-obj (first (.-outputFiles esbuild-result))
bundled-code (.-contents output-file-obj)]
(println (str "esbuild bundling complete."))
;; Step 3: Create executable binary
(fs/writeFileSync output-file
"#!/usr/bin/env -S node --experimental-default-type=module\n")
(fs/appendFileSync output-file bundled-code)
(run-command (str "chmod 755 " output-file)))I'm curious how long this takes to run you y'alls systems:
$ npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000
"Elapsed time: 387.168708 msecs"500.78ms on my MacBook Air M2, 365.87ms on my MacBook Pro M4 Max
gotcha, M4 Pro here as well
$ npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000
"Elapsed time: 384.984667 msecs"
M4 pro here too 😄and bb?
$ bb -e '(time (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j)))))'
"Elapsed time: 353.564958 msecs"
10000000
Similar$ bb -e '(time (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j)))))'
"Elapsed time: 350.56525 msecs"
10000000
$ bb version
babashka v1.12.200
Similar indeed! (you made me realize I hadn't updated brew packages in a long while)nbb
"Elapsed time: 527.623750 msecs"
Only a slow M2 herebb version && bb -e '(time (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j)))))'
babashka v1.12.201
"Elapsed time: 495.332 msecs"
10000000$ npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
10000000
"Elapsed time: 511.161188 msecs"
Dell XPS 13 9305 (from 2021)Tested on few laptops, also de-dusted trusty old Thinkpad X220. Millisecond timings with rough average over few runs. Kernel parameter mitigations=off on all but latest CPU.
| CPU | nm | year | nbb | bb |
|-----------------------------+----+------+------+------|
| AMD Ryzen 7 PRO 7840U | 4 | 2023 | 440 | 454 |
| AMD Ryzen 7 PRO 4750U | 7 | 2020 | 638 | 745 |
| Intel(R) Core(TM) i7-1165G7 | 10 | 2020 | 540 | 565 |
| Intel(R) Core(TM) i5-8350U | 14 | 2017 | 1100 | 1010 |
| Intel(R) Core(TM) i7-2620M | 32 | 2011 | 1450 | 1400 |
Seems nbb runs slightly faster on AMD's, especially on that 4750U (~10%) .
Opposite effect on that i5-8350U which era still seems popular on some cheap VPSs'.
Used benchmark commands:
bb -e '(time (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j)))))'
npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
- babashka-1.12.203-linux-amd64
- nodejs v18.19.0 / nbb@latest 1.3.204Weirdly on one machine I get
npx nbb@latest -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))'
npm ERR! cb.apply is not a function
npm ERR! A complete log of this run can be found in:
npm ERR! /home/user/.npm/_logs/2025-06-27T07_41_27_916Z-debug.log
cat /home/user/.npm/_logs/2025-06-27T07_41_27_916Z-debug.log
0 info it worked if it ends with ok
1 verbose cli [
1 verbose cli '/usr/bin/node',
1 verbose cli '/usr/local/lib/node_modules/npx/node_modules/npm/bin/npm-cli.js',
1 verbose cli 'install',
1 verbose cli 'nbb@latest',
1 verbose cli '--global',
1 verbose cli '--prefix',
1 verbose cli '/home/user/.npm/_npx/5839',
1 verbose cli '--loglevel',
1 verbose cli 'error',
1 verbose cli '--json'
1 verbose cli ]
2 info using npm@5.1.0
3 info using node@v18.19.0
4 verbose npm-session 16b378774e1841ee
5 silly install loadCurrentTree
6 silly install readGlobalPackageData
7 http fetch GET 200 16ms (from cache)
8 silly pacote tag manifest for nbb@latest fetched in 29ms
9 verbose stack TypeError: cb.apply is not a function
9 verbose stack at /usr/local/lib/node_modules/npx/node_modules/npm/node_modules/graceful-fs/polyfills.js:287:18
9 verbose stack at FSReqCallback.oncomplete (node:fs:203:5)
10 verbose cwd /home/user/sites
11 verbose Linux 6.12.13-amd64
12 verbose argv "/usr/bin/node" "/usr/local/lib/node_modules/npx/node_modules/npm/bin/npm-cli.js" "install" "nbb@latest" "--global" "--prefix" "/home/user/.npm/_npx/5839" "--loglevel" "error" "--json"
13 verbose node v18.19.0
14 verbose npm v5.1.0
15 error cb.apply is not a function
16 verbose exit [ 1, true ]
On debian 12, purged the nodejs installation but still persists.
..but there it works still without having the @latest set in the command, just npx nbb -e '(time (print (loop [i 0 j 10000000] (if (zero? j) i (recur (inc i) (dec j))))))' , it seems to point to same nbb version though
Anyways, Moore's Law RIP. Today this should run in 10ms if worked still, looking at the speed on 2011's CPU.
@j.s which node version?
@borkdude nodejs v18.19.0 there also. Seems like something has to be gone bit out of place. I don't mind really as otherwise things seemingly work and reinstalling with next OS update soon anyways. Can send more diagnostics if it smells like a bug that should be investigated.
ok, if it persists on the new OS let me know
yea, sure. that'd be pretty weird if it did
Benchmarking Janet on machine where I got 440ms with nbb and 454ms with bb, running similar program. With it I'm getting 1165ms with recursive function and 762ms when mutating value in loop. Read somewhere Janet would be around same ballpark to CPython but didn't expect it to be this much slower vs. babashka, but well..
# Time macro equivalent from
(defmacro timeit [& body]
# generate unique symbols to use in the macro so they can't conflict with anything used in `body`
(with-syms [$t0 $t1]
~(do
(def $t0 (os/clock :monotonic :double))
(do ,;body)
(def $t1 (os/clock :monotonic :double))
(- $t1 $t0))))
# Use recursive function call as there's tail call optimization
(defn count-recurse [i j]
(if (zero? j)
i
(count-recurse (inc i) (dec j))))
(printf "Took %.3f seconds recursing" (timeit (count-recurse 0 10000000)))
# Mutating
(defn count-loop []
(var i 0)
(var j 10000000)
(while (not (zero? j))
(++ i)
(-- j))
i)
(printf "Took %.3f seconds mutating" (timeit (count-loop)))
## -> Took 1.165 seconds recursing
## -> Took 0.762 seconds mutating bb loops are also faster than python :)
Yup, I remember reading that somewhere
(the normal Python, GraalPython is faster)
right
Janet's lower memory use with basic http server is compelling though, I did get around 15MB (idle memory usage) there while bb http-kit and nbb express were around 50MB, though I don't know if there's room to tune those.
you can set the memory with -Xmx etc args
ok, i'll check those
bb -e '(org.httpkit.server/run-server prn {:port 8889}) @(promise)'
This runs with taking 15mb memory on my systemmac m1 pro
clj (jvm): ~15msecs
cljs (node): ~7msecs ❗ ❓
nbb (1.3.204): ~500msecs
Okay, I'll check how that compares. I hastily measured with https://github.com/babashka/http-server earlier running bb ./http-server --port 8888 --dir . and just the Janet joy example.
Running single bb process globally, memory use in MiB, inspected at KDE System Monitor:
| server | memory | shared | note |
|------------------+--------+--------+---------------------------|
| janet (circlet) | 2.3 | 2.8 | |
| janet (joy) | 11.4 | 3.1 | 2 processes, 5.7 Mib each |
| bb (http-kit) | 31.4 | 29.4 | |
| bb (http-kit) 3x | *16.5 | *30.9 | |
*avg for each of 3 processes
Noted that running bb -e '(org.httpkit.server/run-server prn {:port 8839}) @(promise)' three times parallel shows lower per-process memory use.
I couldn't get Janet networking docs basic net/listen example working but joy framework I guess builds on it, used:
- https://github.com/janet-lang/circlet (c mongoose server)
- https://github.com/joy-framework/joy
Maybe tuning the bb setup could improve it a bit, while it's bit of like comparing apples to oranges.Also quick Python comparison: - Flask ASGI (uvicorn) 29.0MiB / 12.4Mib (shared). - Nanodjango with ASGI and 1 WSGI worker is around 51.6MiB (12.8MiB per worker). There was some Python running in containerd process already which might share few megabytes of RAM for those.
you could try adding -Xmx10m or so but on my machine it didn't have any effect I think
-Xmx should go as the first argument
I don't think it's effective, tried bb -Xmx10M -e '(org.httpkit...
I think it has to be small m
but even then I also didn't see an effect
if you misspell the letter, it will complain though
Yea, bb -h states capital though
interesting, maybe both are valid
no complaints
could there be a hard minimum limit or something, i'm not familiar on stuff that deep
could be
$ rlwrap bb -Xms0m -Xmx0m
Babashka v1.12.202 REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.
user=> (-> (Runtime/getRuntime) (.maxMemory))
13743685632
user=> (/ 13743685632 (* 1024 1024))
13107without those args it's the same though
ah:
$ rlwrap bb -Xmx100m
Babashka v1.12.202 REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.
user=> (-> (Runtime/getRuntime) (.maxMemory))
104857600
user=> (-> (Runtime/getRuntime) (.maxMemory) (/ (* 1024 1024)))
100$ bb -Xmx50m -e '(-> (Runtime/getRuntime) (.maxMemory) (/ (* 1024 1024)))'
50I think 16 is the mimimum. 25 still works, but 10 gives 16
Yea, seems so
Might not be correct way to try optimize server idle memory use this way (restricting heap size), if it's okay that processing takes whatever it takes but minimum memory should be required when listening requests.
But practically I think it's pretty good already. Wouldn't choose Janet only for this sort of memory savings in most cases.
Though it does seem tempting on a machine running out of memory already to have something that barely shows up on system monitor without scrolling all way to bottom.
Managed to get this Janet server code responding, it takes 2MiB (2.6MiB shared):
(defn http-handler
"Handle HTTP connections and return Hello World"
[stream]
(defer (:close stream)
# Read the HTTP request (we'll ignore it for this simple example)
(def request-buffer @"")
(:read stream 1024 request-buffer)
# Send HTTP response with Hello World
(def response "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 12\r\n\r\nHello World!")
(:write stream response)))
# Create and start the HTTP server
(def server (net/listen "127.0.0.1" "8080"))
(forever
(if-let [conn (:accept server)]
(ev/call http-handler conn)))
net/listen sources https://github.com/janet-lang/janet/blob/f2eaa5de/src/core/net.c#L653impressive