Show HN: Coi – A language that compiles to WASM, beats React/Vue
Posted by io_eric 3 days ago
I usually build web games in C++, but using Emscripten always felt like overkill for what I was doing. I don't need full POSIX emulation or a massive standard library just to render some stuff to a canvas and handle basic UI.
The main thing I wanted to solve was the JS/WASM interop bottleneck. Instead of using the standard glue code for every call, I moved everything to a Shared Memory architecture using Command and Event buffers.
The way it works is that I batch all the instructions in WASM and then just send a single "flush" signal to JS. The JS side then reads everything directly out of Shared Memory in one go. It’s way more efficient, I ran a benchmark rendering 10k rectangles on a canvas and the difference was huge: Emscripten hit around 40 FPS, while my setup hit 100 FPS.
But writing DOM logic in C++ is painful, so I built Coi. It’s a component-based language that statically analyzes changes at compile-time to enable O(1) reactivity. Unlike traditional frameworks, there is no Virtual DOM overhead; the compiler maps state changes directly to specific handles in the command buffer.
I recently benchmarked this against React and Vue on a 1,000-row table: Coi came out on top for row creation, row updating and element swapping because it avoids the "diffing" step entirely and minimizes bridge crossings. Its bundle size was also the smallest of the three.
One of the coolest things about the architecture is how the standard library works. If I want to support a new browser API (like Web Audio or a new Canvas feature), I just add the definition to my WebCC schema file. When I recompile the Coi compiler, the language automatically gains a new standard library function to access that API. There is zero manual wrapping involved.
I'm really proud of how it's coming along. It combines the performance of a custom WASM stack with a syntax that actually feels good to write (for me atleast :P). Plus, since the intermediate step is C++, I’m looking into making it work on the server side too, which would allow for sharing components across the whole stack.
Example (Coi Code):
component Counter(string label, mut int& value) {
def add(int i) : void {
value += i;
}
style {
.counter {
display: flex;
gap: 12px;
align-items: center;
}
button {
padding: 8px 16px;
cursor: pointer;
}
}
view {
<div class="counter">
<span>{label}: {value}</span>
<button onclick={add(1)}>+</button>
<button onclick={add(-1)}>-</button>
</div>
}
}component App { mut int score = 0;
style {
.app {
padding: 24px;
font-family: system-ui;
}
h1 {
color: #1a73e8;
}
.win {
color: #34a853;
font-weight: bold;
}
}
view {
<div class="app">
<h1>Score: {score}</h1>
<Counter label="Player" &value={score} />
<if score >= 10>
<p class="win">You win!</p>
</if>
</div>
}
}app { root = App; title = "My Counter App"; description = "A simple counter built with Coi"; lang = "en"; }
Live Demo: https://io-eric.github.io/coi
Coi (The Language): https://github.com/io-eric/coi
WebCC: https://github.com/io-eric/webcc
I'd love to hear what you think. It's still far from finished, but as a side project I'm really excited about :)
Comments
Comment by trzeci 3 hours ago
From the product perspective, it occupies a different market than Emscripten, and I don't see it's good comparison. Your product is borderline optimized to run C++ code on Web (and Coi is a cherry on top of that). Where Emscripten is made to make native C++ application to run on Web - without significant changes to the original source itself.
Now, the `webcc::fush()` - what are your thoughts about scalability of the op-codes parsing? Right now it's switch/case based.
The flushing part can be tricky, as I see cases when main logic doesn't care about immediate response/sharing data - and it would be good to have a single flush on the end of the frame, and sometimes you'd like to pass data from C++ while it's in its life scope. On top of that, I'd be no surprised that control of what flushes is lost.
(I'm speaking from a game developer perspective, some issues I'm thinking aloud might be exaggerated)
Last, some suggestion what would make developers more happy is to provide a way to change wasm compilation flags - as a C++ developer I'd love to compile debug wasm code with DWARF, so I can debug with C++ sources.
To wrap up - I'm very impressed about the idea and execution. Phenomenal work!
Comment by io_eric 2 hours ago
On the opcode parsing - the switch/case approach is intentionally simple and surprisingly fast. Modern compilers turn dense switch statements into jump tables, so it's essentially O(1) dispatch.
Your flush timing concern is understandable, but the architecture actually handles this cleanly. Buffered commands accumulate, and anything that returns a value auto-flushes first to guarantee correct ordering. For game loops, the natural pattern is batch everything during your frame logic, single flush at the end. You don't lose control, the auto-flush on sync calls ensures execution order is always maintained.
DWARF debug support is a great call
Comment by gdotdesign 5 hours ago
Comment by Malp 3 hours ago
Comment by gdotdesign 2 hours ago
Comment by written-beyond 4 hours ago
Comment by publicdebates 4 hours ago
Just curious, what would the FPS be using native plain pure JavaScript for the same exact test?
Comment by io_eric 4 hours ago
The real advantage comes when you have compute-intensive operations, data processing, image manipulation, numerical algorithms, etc. The batched command buffer lets you do those operations in WASM, then batch all the rendering commands and flush once, minimizing the interop tax.
For pure "draw 10k rectangles with no logic," JS is probably fastest since there's no bridge to cross. But add real computation and the architecture pays off :)
Comment by Lalabadie 4 hours ago
Different use cases, obviously, but if a project needs very fast 2D drawing, it can be worth the additional work to make it happen in a Webgl context.
Comment by vanderZwan 1 hour ago
Coi looks pretty nice! But honestly I think WebCC might actually be the thing I have been waiting for to unlock WASM on the web. Because if I understand correctly, it would let me write C++ code that compiles to tiny WASM modules that actually integrates with generic JS code very efficiently. Which would make it much easier to add to existing projects where there are some big bottlenecks in the JS code.
Looking forward to giving it a spin!
Comment by rthrfrd 5 hours ago
But do you think it would be possible to achieve similar results without a new language, but with a declarative API in one of your existing languages (say, C++) instead?
If possible, that would remove a big adoption barrier, and avoid inevitably reinventing many language features.
Comment by io_eric 5 hours ago
A dedicated compiler allows us to trace dependencies between state variables and DOM nodes at compile-time, generating direct imperative update code. To achieve similar ergonomics in a C++ library, you'd effectively have to rely on runtime tracking (like a distinct Signal graph or VDOM), which adds overhead.
Comment by bobajeff 3 hours ago
Comment by rthrfrd 3 hours ago
All the best with it!
P.S. It would also be interesting to see how far it's possible to go with constexpr etc.
Comment by kccqzy 4 hours ago
This itself is quite cool. I know of a project in ClojureScript that also avoids virtual DOM and analyzes changes at compile-time by using sophisticated macros in that language. No doubt with your own language it can be made even more powerful. How do you feel about creating yet another language? I suppose you think the performance benefits are worthwhile to have a new language?
Comment by io_eric 3 hours ago
Comment by elzbardico 2 hours ago
Comment by progx 4 hours ago
<if text.isEmpty()>
<div class="preview-text empty">Start typing...</div>
<else>
<div class="preview-text">{text}</div>
</else>
</if>
As a JS/TS dev it feel unnatural to write language operations as html-tags. That is what i did not like in svelte too.Here something that looks little more like PHP-Style, better separation, but too much to type:
<?coi
if (empty($text)) {
?>
<div class="preview-text empty">Start typing...</div>
<?coi
} else {
?>
<div class="preview-text">${text}</div>
<?coi
}
?>
Shorter with a $-func for wrapping html-content if (empty($text)) {
$(<div class="preview-text empty">Start typing...</div>)
} else {
$(<div class="preview-text">${text}</div>)
}
I don't know, has somebody a better idea?Comment by gdotdesign 4 hours ago
component Main {
fun render : Html {
<div>
if true {
<div>"Hello World!"</div>
} else {
<div>"False"</div>
}
</div>
}
}Comment by zareith 4 hours ago
Comment by frankhsu 5 hours ago
Comment by written-beyond 4 hours ago
What I am wondering is how language interop will work? The only way I see this growing is either you can easily import js libraries or you get a $100,000 dono and let Claude or any other LLM run for a few days converting the top 200 most used react packages to Coi and letting it maintain them for a few months until Coi's own community starts settling in.
I would love to use this for non web use cases though, to this date UI outside of the browser(native, not counting electron) is still doggy doo doo when compared to the JS ecosystem.
Comment by anonymous908213 3 hours ago
Web developers write your own code challenge (impossible)
Comment by zigzag312 4 hours ago
For web small binary size is really important. Frameworks like Flutter, Blazor WASM produce big binaries which limits their usability on the web.
JS/TS complicates runtime type safety, and it's performance makes it not suitable for everything (multithreading, control over memory management, GC etc.)
I wonder how much/if no GC hurts productivity.
It looks like Coi has potential to be used for web, server and cross-platform desktop.
Since the intermediate step is C++ I have a question what this means for hot-reload (does that make it impossible to implement)?
Comment by arendtio 5 hours ago
Comment by orphea 6 hours ago
Comment by k__ 6 hours ago
React and Vue aren't exactly known for their performance and Svelte does compile time optimizations.
Comment by embedding-shape 5 hours ago
Fun how with time, the core purpose of a library ever so slightly change :)
Comment by timeon 3 hours ago
Comment by embedding-shape 3 hours ago
I think this was back in 2013-2014 sometime though, so I might be misremembering, it's over a decade ago after all.
Comment by k__ 5 hours ago
Comment by embedding-shape 4 hours ago
Comment by mirekrusin 4 hours ago
Comment by orphea 2 hours ago
https://github.com/io-eric/coi/commit/65818e4ae3f8cb5f7d2b83...
Comment by iamsaitam 5 hours ago
Comment by monster2control 1 hour ago
Comment by nkmnz 3 hours ago
Comment by io_eric 49 minutes ago
Most practical approach: AI-assisted conversion. Feed an LLM the Coi docs + your Vue code and let it transform components. For migrating existing codebases, that's likely the most efficient path.
For new code, writing Coi directly is simpler :)
Comment by radicalethics 2 hours ago
Comment by LudwigNagasena 3 hours ago
Comment by io_eric 2 hours ago
JSX-like view syntax – Embedding HTML with expressions, conditionals (<if>), and loops (<for>) requires parser support. Doing this with C++ macros would be unmaintainable.
Scoped CSS – The compiler rewrites selectors and injects scope attributes automatically. In WebCC, you write all styling imperatively in C++.
Component lifecycle – init{}, mount{}, tick{}, view{} blocks integrate with the reactive system. WebCC requires manual event loop setup and state management.
Efficient array rendering – Array loops track elements by key, so adding/removing/reordering items only updates the affected DOM nodes. The compiler generates the diffing and patching logic automatically.
Fine-grained reactivity – The compiler analyzes which DOM nodes depend on which state variables, generating minimal update code that only touches affected elements.
From a DX perspective: Coi lets you write <button onclick={increment}>{count}</button> with automatic reactivity. WebCC is a low-level toolkit – Coi is a high-level language that compiles to it, handling the reactive updates and DOM boilerplate automatically.
These features require a new language because they need compiler-level integration – reactive tracking, CSS scoping, JSX-like templates, and efficient array updates can't be retrofitted into C++ without creating an unmaintainable mess of macros and preprocessors. A component-based declarative language is fundamentally better suited for building UIs than imperative C++.
Comment by merqurio 6 hours ago
It reminds me of https://leptos.dev/ in Rust, although the implementation might be very different
Comment by amelius 4 hours ago
That's something I could live with.
Comment by skybrian 4 hours ago
Comment by io_eric 3 hours ago
Comment by hans2002 3 days ago
Comment by doublerabbit 2 hours ago
Fatal error: 'stdint.h' file not found
Yet exists within /usr/includeNot a rant, but developers, please include testing on FreeBSD. Git issue raised.
Comment by io_eric 2 hours ago
The fix was adding freestanding stdint.h and stddef.h to webcc's compat layer using compiler built-ins (__SIZE_TYPE__, etc.). This makes webcc work consistently across all platforms without relying on platform-specific clang configurations.
I hope it works now for you - hit me up if there are still problems!
Comment by doublerabbit 51 minutes ago
Comment by zedai00 6 hours ago
Comment by cap11235 5 hours ago
Comment by Hasnep 3 hours ago
Comment by gethly 5 hours ago
Comment by bookofsleepyjoe 4 hours ago