Skip to content
back to the index
Elena Kuznetsov
By Elena Kuznetsov2026-05-108 min read
Made with
  • tauri
  • rust

Porting my browser pixel-game to Steam with Tauri

Last month I shipped Quiet Frame to Steam after eighteen months of running it as a browser game on Itch. The game itself is small — a 60-MB autobiographical platformer drawn at 64×64 — but the road from "free web build" to "Steam page with achievements" was longer than I expected. This post is a short, honest record of what worked, what did not, and the things I would do differently.

Why I left Electron

I built Quiet Frame's first desktop wrapper in Electron in 2024, because that was what every tutorial pointed to. It worked, but the build was 280 MB, the cold-start was over two seconds even on my M2 Air, and the executable would occasionally redraw the entire window with white flicker when the game paused. None of this is Electron's fault — Electron is a very serious project doing a very hard thing — but for a 64×64 platformer it felt like wearing a tuxedo to fetch the post.

I tried NW.js for a weekend. Same general profile, slightly snappier startup, similar bundle size. Then I tried Tauri. The dev build was 14 MB. The release build was 9 MB. The thing booted in under 200 ms.

I switched.

What Tauri gets right for pixel games

Tauri uses the operating system's native webview — WebKit on macOS, WebView2 on Windows, WebKitGTK on Linux — instead of bundling its own Chromium. That single decision is what makes the bundles small. For a pixel game this is mostly invisible: your canvas renders the same on either engine. The places where it bites are the same places browser games already break (audio latency, gamepad APIs) so if your web build is solid, the Tauri build will be too.

The Rust side — the "host" application — is where you put the things a browser cannot do. For Quiet Frame I needed three things from Rust:

  • Persistent save files in the proper OS location (%APPDATA% on Windows, ~/Library/Application Support on macOS, ~/.local/share on Linux). Tauri's fs plugin gives you exactly this.
  • A SteamWorks bridge for achievements and cloud saves. There isn't a maintained Tauri plugin for SteamWorks, so I wrote a thin wrapper around the excellent steamworks-rs crate.
  • Window-state persistence so the game opens at the same size you closed it. Tauri's window-state plugin ships with this.

Total Rust code for all three: under 300 lines.

The SteamWorks shim, simplified

The SteamWorks bridge was the only piece that took real effort. The trick is to keep the API surface small. My entire bridge exposes four commands to the front-end:

rust
#[tauri::command]
fn steam_unlock_achievement(name: String) -> Result<(), String> { ... }

#[tauri::command]
fn steam_set_stat(name: String, value: i32) -> Result<(), String> { ... }

#[tauri::command]
fn steam_user_id() -> Result<String, String> { ... }

#[tauri::command]
fn steam_run_callbacks() -> Result<(), String> { ... }

From the JavaScript side, unlocking an achievement is just:

javascript
import { invoke } from '@tauri-apps/api/core'
await invoke('steam_unlock_achievement', { name: 'finished_chapter_one' })

You also have to call steam_run_callbacks on a tick — I do it every animation frame, which is overkill but cheap. SteamWorks itself swallows the extras gracefully.

The trickier issue was that on macOS, the Steam client expects a steam_appid.txt file next to the executable during development but not in shipping builds. The Tauri build script handles this with a small tauri.conf.json resource block; the Tauri docs page on bundled resources covers it well.

The numbers

Before / after, for the same game:

ElectronTauri
Installed size (Win64)286 MB18 MB
Installed size (macOS)312 MB22 MB
Cold start (M2 Air)2.1 s0.2 s
Memory at idle380 MB64 MB
Game CPU at 60 fps14 %9 %

The bundle-size drop is the headline, but the memory number is what made the decision feel right. Players who keep Quiet Frame minimised in the background are using a quarter of the RAM they used to.

What still hurt

Audio latency on Linux's WebKitGTK is rough. Around 90 ms in my testing. I had to push the player's input prediction window out by a couple of frames to compensate. This is a known WebKitGTK issue and not Tauri-specific, but it is something to know about before you ship.

Gamepad rumble on Windows requires WebView2 Evergreen. Most Win10 and Win11 machines have it; older corporate fleets sometimes don't. I gate rumble behind a feature-detect rather than relying on a specific runtime version.

The macOS notarisation pipeline is finicky. Tauri's signing guide is correct, but you'll spend half a day understanding what Gatekeeper expects from a bundle that didn't originate from Xcode. Plan a day for it. You won't need it next time.

The Steam cert review

Steam's certification process for a small indie game is mostly about filling out the storefront properly and writing a short build description. The hard parts are the achievements (which need icon assets at three sizes and have to match the SteamWorks definitions exactly), and the Steamworks Configuration page for the various builds (default, beta, demo, etc).

Quiet Frame cleared cert on the second pass — the first pass came back because I'd shipped a build that asked for write access to ~/.local/share on Linux without declaring it in the manifest, which Steam's automated check correctly flagged. Fixing the manifest line and rebuilding took ten minutes.

Would I do it again

Yes — with one change. I would have started in Tauri eighteen months ago instead of detouring through Electron. The game would still have launched on the same day, the install size would have been a third, and I would have skipped about two weeks of fighting with Chromium memory profiles.

If you are working on a small browser-based game and looking at "should I ship to Steam?", the answer is probably yes, and Tauri makes the answer cheaper than it has any right to be.

Appreciated by103 people liked this piece
Share:MastodonEmail