Building VeloxKit
For the past 18 months I've been building a desktop framework for React developers. Not another Electron wrapper, not a WebView shell β something genuinely different: a GPU renderer and a custom React reconciler running on a stripped-down V8 runtime.
This is the story of why, and how.
Starting from tradeoffs
Electron is an engineering triumph. Shipping a full Chromium browser with every app was the right call in 2013. You got a universal rendering engine, a full Node.js runtime, and the ability to hire any web developer. VS Code, Slack, Notion β the list of successful Electron apps is long for good reason.
The tradeoffs are also real. Chromium alone is 130MB. A hello-world Electron app uses around 100MB of RAM at idle. Cold startup takes 600β800ms while V8 warms up. For many apps β especially developer tools and productivity software where users are on capable hardware β these numbers are acceptable. For apps where footprint matters, they aren't.
Tauri and Electrobun address the binary size problem by using the OS WebView. Smaller binary, but you're still on a browser engine β Safari's WebKit on macOS, Edge WebView on Windows. Rendering behaviour depends on the platform's installed browser version.
I wanted to explore a different point in the design space: a native renderer with no browser lineage, using React as the programming interface.
wgpu + Vello: the GPU layer
wgpu (opens in a new tab) is a Rust implementation of the WebGPU API. It runs on Vulkan, Metal, and DirectX 12. Vello (opens in a new tab) is a 2D renderer built on wgpu β it draws paths, text, and images using compute shaders.
Together, they give you a 2D scene graph with GPU acceleration, on any platform, without a browser.
The VeloxKit renderer takes a positioned element tree and issues Vello draw calls. A View becomes a filled rectangle. A Text becomes a Vello glyph run. A Pressable is a View with an event handler attached. Everything is a GPU draw call.
Crucially: Vello and wgpu are the same GPU pipeline as Canvas3D. There's no "2D mode" and "3D mode" β they're different users of the same wgpu command encoder.
The React reconciler
React's reconciler is its best-kept secret. Most developers think of React as a DOM library. It's not β it's a diffing engine with a pluggable host. React Native uses the same diffing engine as React DOM but produces native iOS/Android view hierarchies. VeloxKit does the same thing for our element tree.
The react-reconciler package exposes a host config interface. You implement about 20 methods:
const reconciler = ReactReconciler({
createInstance(type, props) {
// Map 'View', 'Text', etc. to native element structs
return velox_create_element(type, props)
},
commitUpdate(instance, updatePayload, type, oldProps, newProps) {
// Props changed β push the diff to Rust
velox_update_element(instance, updatePayload)
},
appendChild(parent, child) {
velox_append_child(parent, child)
},
// ... ~15 more methods
})The Rust side receives these mutations and updates the element tree, which the layout engine (Taffy) processes on the next frame.
Getting this right took about two months. The tricky parts:
- Concurrent mode β React's scheduler interleaves work across frames. The reconciler must be non-blocking. Our Rust calls are synchronous by design (no async FFI in the hot path), so we batch them at commit time.
- Text measurement β React needs to know text sizes before layout. We call into Rust synchronously to measure glyph runs. This is one of two synchronous Rust calls in the render path; everything else is batched.
- Portal semantics β
veloxWindow.Portalrenders into a secondary window's element tree, not the main one. This required a second reconciler instance per window.
V8 snapshots: the startup trick
A V8 snapshot is a serialized heap image. At build time, you run V8, execute your JavaScript, and serialize the resulting heap to a binary file. At startup, instead of parsing and compiling JavaScript, V8 deserializes the heap directly into memory.
The deserialization is fast. The first time V8 runs your code, it JIT-compiles everything. The snapshot captures the already-compiled functions. On subsequent starts (and for distributed apps, every start), V8 skips straight to executing.
For VeloxKit, the snapshot includes:
- The VeloxKit runtime library (~200KB of JS)
- React and the custom reconciler (~150KB)
- Your application code (varies)
The result: cold startup under 50ms for a typical app, versus 600β800ms without the snapshot.
The gotcha: snapshot generation takes ~15 seconds. This is the veloxkit build step. Development mode skips snapshot generation and uses HMR instead.
The binary trailer model
One of the design goals was a single-file executable. No installer that puts files in five different places. One file, double-click, run.
The mechanism: the VeloxKit runtime binary has a 64-byte trailer at the end. When it starts, it reads the trailer to find an offset within itself. At that offset is the app blob β your code, assets, snapshot. The OS sees one file; the runtime finds two logical sections.
This is the same model Bun uses for its single-file executables, independently arrived at. It's a clean solution.
The runtime binary is ~18MB. A typical app blob is 1β3MB. Total: ~20MB.
Where it is now
Since writing this, a lot has shipped. The reconciler is stable. V8 snapshots and the binary trailer model are in production. The capability system covers filesystem, network, database, audio, AI inference, camera, keychain, and more.
Some highlights from recent releases:
- Local AI β embed, generate, and transcribe audio, all on-device via Candle
- Canvas 2D + 3D β GPU-accelerated drawing surfaces alongside the UI tree
@velox/designβ a design system with Catppuccin-based tokens, dark/light/system themes, and base components- Backend commands β a typed
backend.*pattern for registering Rust async functions callable from React - JS plugins β extend any app without touching the Rust layer
- Hot reload β sub-100ms edit-to-screen cycle in dev mode, using a Chrome DevTools Protocol inspector
The core model β React reconciler, GPU renderer, V8 snapshot, single binary β is unchanged and will not change before v1.0.
The framework is still pre-release. APIs marked Experimental may shift. If you're building something with it, I want to hear about it.
If you build something with it, I want to hear about it.
β Toby Emmanuel
GitHub (opens in a new tab) Β· Twitter (opens in a new tab) Β· Discord (opens in a new tab)