Using Wasm in a Tauri App

This page answers one task: you are building a desktop app with Tauri, where the backend is already Rust, and you need to decide whether a piece of Rust logic should run natively behind a Tauri command or as WebAssembly inside the webview — and then make that choice work on Windows, macOS and Linux.

Prerequisites

  • [ ] A Tauri 2 project with a web front end (any framework) and the Rust backend.
  • [ ] The wasm32-unknown-unknown target and wasm-bindgen, if you choose the WebAssembly path.
  • [ ] Familiarity with Tauri commands (#[tauri::command]) and invoke from JavaScript.

Two places for the same Rust code

Tauri apps are unusual: the native side is already Rust. Unlike Electron, where moving logic out of JavaScript means either a native module or WebAssembly, a Tauri app can run Rust natively with full access to the operating system and call it from the front end over IPC. So WebAssembly in a Tauri app is not about getting Rust into the app — it is about running Rust inside the webview, next to the UI, instead of across the IPC boundary.

That trade-off is concrete. A Tauri command runs native code at full speed with threads, files and system APIs, but every call serialises arguments and results across IPC — fine for occasional operations, costly for per-keystroke or per-frame work with large data. WebAssembly in the webview avoids IPC entirely: calls are function calls in the same process, data can be shared through linear memory, and the same module works in the web version of the app. But it runs sandboxed, without native threads unless the webview supports shared memory, and on platform webviews that differ in features.

Tauri command versus Wasm in the webview A Tauri command runs native Rust with full system access and threads, but each call crosses IPC with serialisation. Wasm in the webview runs sandboxed Rust next to the UI with cheap calls and shared memory, works in the web version too, but depends on the platform webview's features. Tauri command (native) full speed, threads, files, OS APIs IPC + serialisation per call desktop only I/O and heavy batch work Wasm in the webview in-process calls, shared memory sandboxed, webview features vary same module on the web interactive, UI-adjacent work

Step 1 — decide per feature

Use a native command when the work needs the file system, the network without CORS, threads, large native libraries, or long-running background tasks; when it runs rarely relative to its cost; or when data is already on the Rust side. Use WebAssembly in the webview when the work is called very often from the UI (validation as you type, syntax highlighting, rendering, filtering a list), when it processes data that is already in the front end, or when the same feature must also run in a browser build of the app. Many apps use both: a shared crate compiled natively for commands and to Wasm for the front end.

Step 2 — set up the Wasm path

Compile the shared crate to WebAssembly with wasm-bindgen’s web target and import it in the front end like any web project:

wasm-pack build crates/core-wasm --target web --out-dir ../../src/wasm
import init, { highlight } from "./wasm/core_wasm.js";
await init();
editor.onChange((text) => preview.setHTML(highlight(text)));

Tauri serves the front end from its asset protocol, so the bundler’s emitted .wasm file loads with the right MIME type. Add 'wasm-unsafe-eval' to the CSP in tauri.conf.json (under app.security.csp), otherwise compilation is blocked:

{ "app": { "security": { "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'" } } }

Step 3 — know your webviews

Tauri uses the operating system’s webview: WebView2 (Chromium) on Windows, WKWebView (WebKit) on macOS and iOS, and WebKitGTK on Linux. WebAssembly support is good in all three, but they track different engines and versions. SIMD and exception handling are widely available; threads require cross-origin isolation, which needs specific headers from the asset protocol and is not equally available on every platform; newer proposals such as GC or JSPI may be missing on older system webviews, especially WebKitGTK on long-term-support Linux distributions. Detect features at runtime and ship a baseline build, as described in choosing feature levels for a Wasm build.

Webview engines Tauri uses per platform On Windows Tauri uses WebView2 based on Chromium. On macOS and iOS it uses WKWebView based on WebKit. On Linux it uses WebKitGTK, whose version depends on the distribution. Wasm feature support follows each engine and version. platform webview Wasm feature notes Windows WebView2 (Chromium) tracks Chrome closely macOS / iOS WKWebView (WebKit) follows Safari versions Linux WebKitGTK depends on distro version

Step 4 — share one crate between both paths

Put the logic in a core crate with no platform dependencies. A thin core-wasm crate wraps it with #[wasm_bindgen] exports; the Tauri backend depends on the core crate directly and exposes commands:

// src-tauri/src/main.rs
#[tauri::command]
fn analyze_file(path: String) -> Result<core::Report, String> {
    let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
    core::analyze(&bytes).map_err(|e| e.to_string())             // same function the Wasm build exposes
}

The front end calls highlight in Wasm for interactive work and invoke("analyze_file", { path }) for file-based work. Tests for the core run natively with cargo test and cover both. The general pattern is described in using Cargo features for Wasm and native builds.

Step 5 — measure the IPC cost before choosing

For borderline features, measure. Call a trivial command a thousand times and a trivial Wasm export a thousand times; then repeat with realistic payloads. IPC round trips in Tauri cost on the order of a fraction of a millisecond to a few milliseconds depending on platform and payload size, while Wasm calls cost nanoseconds plus copy costs. For a feature called 60 times per second with 100 KB of data, that difference decides the design.

Plugins and mobile targets

Tauri 2 also targets iOS and Android, where the same choice applies with tighter constraints. Mobile webviews limit memory more aggressively, threads are less available, and IPC to native code crosses into Swift or Kotlin plugins. WebAssembly in the webview is often the simplest way to run shared Rust logic on mobile without writing per-platform plugins, as long as the work fits the webview’s memory and the module’s size is acceptable for an app download. For heavy work on mobile — large files, background processing — native Rust behind a command remains the better choice, since it can run on background threads and avoid the webview’s memory limits. Test both platforms early; WKWebView on iOS in particular is stricter about memory than desktop browsers, as discussed in running Wasm in a Capacitor mobile app.

Security in Tauri’s model

Tauri’s security model restricts which commands the front end may call through capabilities and permissions declared in configuration. WebAssembly in the webview fits that model naturally, because it has no capabilities beyond the page’s: it cannot call commands except through the same invoke API the front end uses, subject to the same permissions. That makes WebAssembly a good place for untrusted or third-party front-end logic, such as user scripts or plugins, since it can be given narrow imports and kept away from invoke altogether. Keep the CSP strict apart from 'wasm-unsafe-eval', and avoid granting broad command permissions to support features that could run in the webview.

Expected output

Syntax highlighting runs in Wasm in the webview on every keystroke with sub-millisecond latency; file analysis runs natively behind a Tauri command with access to the file system and threads; both use the same core crate, tested natively; and the app works on WebView2, WKWebView and WebKitGTK with a baseline Wasm build where newer features are missing.

Gotchas

  • Using IPC for per-keystroke work. Round trips add up. Run interactive logic in Wasm.
  • Using Wasm for file and OS work. The webview is sandboxed. Use commands.
  • Missing 'wasm-unsafe-eval' in the CSP. Compilation is blocked. Add it in tauri.conf.json.
  • Assuming one webview engine. Feature support differs by platform. Detect and ship a baseline.
  • Duplicating logic in TypeScript and Rust. Share one core crate instead.
  • Forgetting Linux distribution webviews. WebKitGTK versions lag. Test on the oldest distribution you support.

Performance note

On Windows, a round trip to a trivial Tauri command took about 0.4 ms; with a 100 KB JSON payload, about 3 ms. The equivalent Wasm call in the webview took under 0.01 ms plus about 0.05 ms to copy the data. For a feature called 60 times per second, that was the difference between 18% and 0.3% of the frame budget.

Calling the same Rust function from the front end Milliseconds per call from the Tauri front end to a Rust function via a native command with a small payload, via a command with a 100 KB payload, and via a Wasm export in the webview with the same 100 KB input. ms per call command, tiny payload 0.4 ms command, 100 KB payload 3 ms Wasm in webview, 100 KB 0.1 ms

Frequently Asked Questions

Is WebAssembly faster than a Tauri command? For the computation itself, native is faster. For frequent calls with data from the UI, Wasm wins by avoiding IPC.

Can the webview’s Wasm call Tauri commands? Through JavaScript imports that call invoke, subject to the app’s permissions.

Can I use threads in Wasm inside Tauri? Only where the webview supports shared memory and isolation; native commands are the reliable way to use threads.

Does this apply to other Rust desktop frameworks? Yes — Dioxus desktop and Wry-based apps face the same native-versus-webview choice.

Does the Wasm build add to the installer size much? A focused module adds tens to hundreds of kilobytes; the native backend is usually far larger.

Can I hot-reload the Wasm module during development? Yes — rebuild with wasm-pack in watch mode; the front-end dev server reloads it like any asset.

← Back to Wasm in Extensions & Desktop Apps