capnwasm
Real Cap'n Proto C++ in the browser. Typed RPC, binary wire.
One typed-client toolchain for any backend — write a .capnp schema, get
a fast typed JS client back. npx capnwasm gen user.capnp emits a reader/builder
that runs against real upstream Cap'n Proto C++ statically compiled to wasm.
Not production-ready yet. capnwasm is intended to become production-capable,
and normal readers now keep message bytes in managed WebAssembly.Memory. Treat these
pages as controlled demos while allocator, large-data, hostile-input, and secure-memory
hardening continues.
What this project is
capnwasm is me trying to learn Cap'n Proto and capnweb by building the opposite
experiment: keep the real Cap'n Proto binary wire in the browser, compile the upstream
C++ runtime to wasm, and measure what changes.
This is not a scoreboard where Cap'n Proto always wins. The useful question is the
tradeoff boundary: when does zero-copy / sparse access / raw binary pay off, and
when is JSON or capnweb the better choice?
JSON / capnweb is usually better for
- tiny JS-to-JS objects
- control-plane calls where payload is a few bytes
- smallest bundle / no wasm runtime
Cap'n Proto / capnwasm is interesting for
- raw binary data without base64
- large payloads where you read only a few fields
- wire interop with non-JS Cap'n Proto services
Live comparison
First this page runs a small fetch/decode/render pass using the same fixture rows as
the playground. Then it starts the RPC smoke against the current Worker.
1. Fetch playground smoke: same data, three encodings
REST / JSON
measuring…
time: —
capnweb
measuring…
time: —
capnwasm
measuring…
time: —
Running fetch smoke…
2. RPC smoke: runs after payload sizes load
RPC burst, 200 concurrent calls (browser→Worker WS)
waiting for payloads…
live
RPC 64 KB binary echo (browser→Worker WS)
waiting for payloads…
live
Bundle — full RPC client (JS + wasm, gzip)
loading…
build
Wasm init on this page (fetch + compile)
measuring…
live
📊
Render bench
End-to-end full pipeline: capnweb × capnwasm crossed with
WebSocket × HTTP-batch, three sizes, cold & warm.
Both libraries win some, lose some — the whole picture.
Tradeoffs
📖
Reader access snippets
Read-only TypeScript snippets for draft(), direct getters,
toObject(), and builder edits. Each snippet runs against
real Cap'n Proto bytes in your browser.
Snippets
🌍
A world where everyone speaks Cap'n Proto
A globe of Cloudflare PoPs. Send a message; every PoP runs a
different language — TS, Python, Ruby, Go, Java — on
its own Worker. Same wire bytes encoded by
capnp.slim.wasm, every runtime decodes them, every
reply bubbles up at its real Cloudflare location.
Live demo
💬
Live chat demo
createClient + subscribeQuery + optimistic
in a working chat. Open in two tabs to watch messages stream live across.
Interactive
🧩
Dynamic schema demo
No codegen: define the schema as JS data, build Cap'n Proto bytes,
pick selected fields, and round-trip through the Worker.
No codegen
🔁
OpenAPI conversion
Paste OpenAPI JSON, convert to manifest, emit canonical .capnp,
and round-trip back to canonical OpenAPI in the browser.
Contract
⚖️
vs capnweb
Where capnwasm wins, where it loses, where it’s tied. Reproducible
numbers. Bundle size and cold start go to capnweb.
Tradeoffs
🔍
Wire inspector
One line in DevTools, walks any Cap'n Proto frame.
Schemaless or schema-aware. Hosted as a single /inspect.js
on this site — not in the npm package.
Debug tool
📝
Notes from the trenches
Field report on the perf work: allocation traps, GC churn, a fast-path
that wasn't, one negative SIMD result, and the tradeoff summary.
Write-up