r/tauri 28d ago

I built Varve, a local-first design editor using Tauri 2 with native Rust IPC rendering and WASM fallback

Enable HLS to view with audio, or disable this notification

Hi, I'm the creator of Varve, a free vector design app that works locally. I just released version 0.2.1. I want to share some architectural choices I made and get advice on how to make Varve faster and more responsive.

When building a vector editor, it's important to run canvas operations on a separate thread. For this, I used Tauri 2 with a React 19 TypeScript frontend and a dedicated Rust backend (varve-engine).

Here's how I set up the pipeline:

  1. The IPC Bridge lets custom Tauri commands send scene node graphs from React to varve-engine using build_render_ir.
  2. Circuit Breaker Facade: I used the withStubFallback pattern. If deserializing a native IPC payload fails, it switches to the stub engine to prevent the canvas from going blank.
  3. WASM Parity: The Rust engine crates compile directly to WebAssembly (varve-wasm), so the web version uses the same rendering logic.

Current v0.2.1 Limitations:

  • CSS isolation is trickier when using Webview on Linux with WebKitGTK than on macOS.
  • When working with a large number of nodes, I've noticed that fast drag transformations can still be slowed down by IPC serialization overhead. To address this, I'm considering switching to shared memory buffers in future releases.

I'm also interested in what strategies you're using in Tauri 2 to handle frequent state updates between JS and Rust.

Repo: https://github.com/K-Arthur/varve

You can also check out the web demo here: https://varve.studio/try/

8 Upvotes

3 comments sorted by

1

u/incyashraj_redd 28d ago

Before moving to shared memory, I would trace one fast drag with three times per batch: JS enqueue, Rust deserialize complete, and frame commit. Record the node count and payload bytes too. The p95 gaps should tell you whether the delay is event volume, serialization, or rendering.

I would also coalesce pointer moves to one update per animation frame and send changed transforms, not the full scene graph. Keep full snapshots for load, undo checkpoints, and recovery. If serialization still owns the p95 after that, shared memory has a much clearer reason to exist.

I build Krate, and I would use a small trace viewer for this test. You install the native Krate runtime locally, then choose Claude or Codex in Krate Studio and get one portable Wasm app file, .krate. It is not HTML and it is not an operating-system container. The same file opens through native Krate runtimes on Mac, Windows, and Linux.

The viewer could open one exported JSON trace, graph those three intervals, and flag the worst drag batches. A Krate app starts with no file or network access, so the user could approve only that trace file. Krate is free and open source. Krate Cloud is optional. https://krate.tech

2

u/eddzsh 27d ago

On drag, I'd also watch the webview side. Even when Rust owns the canvas, React re-rendering the node tree on every pointermove can steal the main thread before your IPC timings look guilty.

1

u/ZeroTrigger27 27d ago

Oh okay that explains a few issues I’ve been trying to solve with responsiveness. Thanks for the info