Nov 9, 2025
What you see in the demo below is ClojureScript UIx app driving native window with ImGui UI via custom React reconciler. Both hot-reloading and REPL-driven development are supported, as you’d expect it from a typical ClojureScript project.
The JavaScript side of the app runs in Hermes engine, which exposes ImGui to JS env. However, a release build of this exact app is a fully native 8MB executable. You can try it yourself on macOS or Linux.
While Hermes engine is a JavaScr…
Nov 9, 2025
What you see in the demo below is ClojureScript UIx app driving native window with ImGui UI via custom React reconciler. Both hot-reloading and REPL-driven development are supported, as you’d expect it from a typical ClojureScript project.
The JavaScript side of the app runs in Hermes engine, which exposes ImGui to JS env. However, a release build of this exact app is a fully native 8MB executable. You can try it yourself on macOS or Linux.
While Hermes engine is a JavaScript VM, it is also an AOT compiler for JavaScript that emits either Hermes byte code or C.
Compiling this sample JavaScript program with Static Hermes: shermes -emit-c index.js
print(1 + globalThis.value);
outputs the following C program or 50KB executable, when compiled straight into a binary shermes -Os index.js
locals.t0 = _sh_ljs_get_global_object(shr);
frame[3] = _sh_ljs_try_get_by_id_rjs(shr,&locals.t0, get_symbols(shUnit)[1] /*print*/, get_read_prop_cache(shUnit) + 0);
locals.t0 = _sh_ljs_try_get_by_id_rjs(shr,&locals.t0, get_symbols(shUnit)[2] /*globalThis*/, get_read_prop_cache(shUnit) + 1);
locals.t0 = _sh_ljs_get_by_id_rjs(shr,&locals.t0,get_symbols(shUnit)[3] /*value*/, get_read_prop_cache(shUnit) + 2);
np0 = _sh_ljs_double(1);
frame[1] = _sh_ljs_add_rjs(shr, &np0, &locals.t0); // 1 + globalThis.value
np0 = _sh_ljs_undefined();
frame[4] = _sh_ljs_undefined();
frame[2] = _sh_ljs_undefined();
locals.t0 = _sh_ljs_call(shr, frame, 1);
_sh_leave(shr, &locals.head, frame);
return np0;
The above demo is 8MB executable on macOS, where 3MB is Hermes runtime, 1.8MB is React the JavaScript library compiled to C and the rest 3.2MB goes to native libraries ImGui and libwebsockets, and ClojureScript code compiled to C.
Here’s a rough diagram of how things are bundled in the executable.

You may noticed there’s untyped and typed JavaScript. When AOT compiling typed JavaScript, which can be a subset of either TypeScript or Flow, Hermes emits a more optimal C making it suitable for performance sensitive code, such as bindings to native libraries runnning in a hot loop.
As an example, compiling the following typed program with -typed flag: shermes -typed -emit-c index.js
function add(a: number): number {
return a + globalThis.value;
}
print(add(1));
emits C that:
- doesn’t create an
addfunction/closure, instead the arithmetic is emitted inline - adds type assertion for unkown value
globalThis.value - includes fewer symbols & caches
You can check both typed and untyped C here, here’s also a diff to make it more obvious.
As I said earlier, the app runs raw JavaScript in dev to provide fast feedback loop and support interactive development. This is possible because Hermes has multiple tiers of code optimization:
- During developent JavaScript VM loads the code and emits byte code (JIT) at runtime
- You can also compile JavaScript to bytecode (.hbc file) ahead of time. This use case is quite popular in React Native, where application code is compiled into Hermes bytecode at build time to improve startup time of mobile apps, especially on Android.
- Finally, with Static Hermes you can compile JavaScript to native object file. This way you get statically linked machine code, skipping parsing and JIT.
Now the best part about this setup is actually React itself. I didn’t have to change anything in UIx to make it work with original imgui-react-runtime demo. The real power and a curse of React is that it is a platform independent abstraction. It’s up to the host platform to provide specific realization of the contract. If you are Clojure developer, this might sound familiar to you.
The abstraction in question is React’s reconciler. By implementing its interface you can create custom render targets, such as React Native, PixiJS React, react-three-fiber, you can even create Terminal UIs and PDFs with it. Heck, if you take a step back and think about the reconciler as a generic abstraction for turning declarative description of something into a set of imperative operations, you can even run hardware with it.
Ok, so how fast is AOT compiled JavaScript with Static Hermes? Here’s a quick benchmark:
(simple-benchmark []
(->> (range)
(map inc)
(filter odd?)
(map #(zipmap (repeatedly % Math/random) (repeatedly % Math/random)))
(take 1e2)
(reduce #(apply + %1 (mapcat identity %2)) 0))
1e2)
Optimized executable runs for ~6450 ms. Same code executed as JavaScript in Node and Bun runs for ~1100 ms. That’s 6x slower, ouch. On the bright side, the executable is just a few MBs, while embedding JS VM will get you at least up to 60MB in the case of Bun. If you ask me, I’ll always prefer 6x faster app, doesn’t matter how big it is.
However, the case with React is quite specific. React being a declarative abstraction on top of imperative APIs almost always means that hot code paths are executed in underlying layers, whether it’s browser’s DOM, mobile views, WebGL or ImGui. And often times you have an option to use that lower level, more performant API directly, when needed.
Hermes was originally developed for React Native and mobile apps, with a focus on improving startup performance. Even generated native code can’t compete with JITs in modern JavaScript engines.
Bonus point. Similarly to how shadow-cljs displays compiler warnings and errors in a browser, the demo ImGui app also captures and displays warnings and errors, as well as includes an error boundary component that catches runtime exceptions thrown during render phase.
Checkout the code of this project at roman01la/cljs-static-hermes, which is based off the work done by a member of Hermes team, at tmikov/imgui-react-runtime. If you are interested in driving native window from runtimes like Node or Bun, checkout this project showcasing use of Bun’s FFI to interface with GLFW and OpenGL: roman01la/hra.