DEV Community

Dzmitry Salavei
Dzmitry Salavei

Posted on

I rewrote my Electron app in Tauri and cut the installer from 120MB to 8MB — here's what actually broke

The problem with Electron

I built the first version of LazyWords in Electron. It worked. It also shipped a 120MB installer for an app whose entire job is showing a small floating flashcard every few minutes.

That felt wrong. So I rewrote it in Tauri v2.

Final result: 8MB installer, same functionality, noticeably less RAM. But the migration wasn't smooth — here's what actually bit me.


What is LazyWords

Before the technical stuff: LazyWords is a passive vocabulary app for Windows. It runs in the system tray and shows word flashcards as an always-on-top overlay over whatever you're working on. Cards appear every few minutes, stay a few seconds, disappear. No interaction required.

The idea was "radio for vocabulary" — you don't focus on it, but it's there, and things stick over time.

LazyWords demo


What broke during migration

1. Global shortcuts behaved differently

Electron's globalShortcut and Tauri's plugin handle edge cases differently. My main issue was Ctrl+Shift+N — show next card immediately. In the Electron version this was straightforward. In Tauri I needed the shortcut to both show a card and reset the timer interval, without triggering a double card.

The fix was a manual_trigger Tokio Notify in the timer loop:

tokio::select! {
    _ = tokio::time::sleep(Duration::from_secs(1)) => {}
    _ = manual_trigger.notified() => { break; }
}
Enter fullscreen mode Exit fullscreen mode

When the shortcut fires, it notifies the trigger, the timer resets its interval. Clean, no double card.

2. Always-on-top + fullscreen detection

Getting a transparent frameless window that floats over normal apps but disappears during actual fullscreen (games, video players, presentations) had no built-in Tauri solution. I ended up calling winapi directly:

unsafe {
    let hwnd = GetForegroundWindow();
    let mut monitor_info: MONITORINFO = std::mem::zeroed();
    monitor_info.cbSize = std::mem::size_of::<MONITORINFO>() as u32;
    let monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
    GetMonitorInfoW(monitor, &mut monitor_info);
    // compare window rect to monitor rect
}
Enter fullscreen mode Exit fullscreen mode

Not pretty, but it works reliably.

3. Settings window deadlock on first open

If you create the settings window on demand (on Ctrl+Shift+W), there's a subtle deadlock risk on the first call when AppState is involved. The fix was to pre-create the settings window hidden at startup:

// At startup — create hidden, never destroy
settings_window.hide()?;

// On shortcut — just show it
settings_window.show()?;
settings_window.set_focus()?;
Enter fullscreen mode Exit fullscreen mode

Also intercepting CloseRequested with prevent_close() + hide() so the window is never destroyed — just hidden. This also fixed the window only being openable once.

4. Single-instance lock

Electron has this built in. In Tauri on Windows I handled it manually with a named mutex:

let mutex_name = windows::core::w!("LazyWords_SingleInstanceMutex");
let mutex = CreateMutexW(None, true, mutex_name)?;
if GetLastError() == ERROR_ALREADY_EXISTS {
    return Ok(());
}
Enter fullscreen mode Exit fullscreen mode

Second launch exits immediately, first instance continues normally.


What worked better than expected

Multi-monitor positioning. Tauri's cursor_position() + available_monitors() just worked. The card always appears on whichever monitor the cursor is on, no hacks needed.

The async timer loop. Tokio's select! macro made the timer logic genuinely elegant — sleeping on interval OR manual trigger, whichever comes first. This would have been messier in Electron.

Bundle size. 8MB vs 120MB speaks for itself. Tauri uses the system WebView (WebView2 on Windows) instead of bundling Chromium.


The AI-assisted workflow

One more thing worth mentioning: this app was built almost entirely with Claude.

The workflow that worked for me:

  1. Describe a feature or bug to Claude in chat
  2. Claude produces a Markdown task spec — what to implement, edge cases, approach
  3. Paste the spec into Claude Code in VS Code terminal
  4. Claude Code implements it
  5. Test, report results, iterate

The key insight: Claude chat is good at architecture and tradeoffs. Claude Code is good at executing a well-scoped task. Separating the two produced much better results than asking Claude Code to also design the solution.

I also keep a CLAUDE.md in the repo root that Claude Code reads at the start of every session — current architecture, known bugs, version history. Without it, context resets every session.


Result

Before (Electron) After (Tauri v2)
Installer size ~120MB ~8MB
RAM idle ~200MB ~30MB
Unit tests 0 27

Live on Microsoft Store and GitHub.

Repo: github.com/DemonazGH/LazyWords-Tauri

Happy to answer questions about any of the Tauri-specific solutions above.

Top comments (0)