SuperAPI AI Directory

lov-optimize-tauri-backend

Coding & development

View repository

Updated 10-16 00:01

About this skill

optimize-tauri-backend — Tauri backend optimization

Use this skill to turn a growing Tauri backend into a smaller, more stable, more pleasant development surface. The goal is not magical Rust HMR. The goal is to reduce how often Rust changes are needed, make Rust restarts less disruptive, and keep the Tauri command boundary small, typed, and auditable.

When to Use

  • The user is developing with pnpm tauri dev / npm run tauri dev and Rust file changes keep closing and reopening the app.
  • src-tauri/src/lib.rs has become a God file.
  • The app exposes 100+ or 200+ #[tauri::command] functions.
  • Frontend code scatters raw invoke("command_name") strings across many components.
  • Long-running invoke / Channel calls cause stale callback warnings after reload or HMR.
  • The user wants backend modularization, command consolidation, better dev scripts, or restart-state recovery.

Core Position

State this clearly when asked about hot reload:

Surface Dev behavior
Frontend Vite / React real HMR
Rust / Tauri backend recompile + restart native process
Best optimization separate stable Tauri shell from volatile domain logic, then make restarts cheap

Do not promise Rust backend HMR inside Tauri. Tauri's Rust-side "hot reload" means watch, rebuild, and restart.

Workflow

Step 1: Read Local Rules First

Before changing files, inspect project instructions and config:

pwd
find .. -name AGENTS.md -print
find .. -name CLAUDE.md -print
rg -n "beforeDevCommand|devUrl|frontendDist|tauri dev|--no-watch|invoke\\(|#\\[tauri::command\\]|generate_handler" package.json src-tauri src 2>/dev/null

Honor local constraints. If the repo says not to run pnpm build, do not run it. Prefer rg and small file reads. Do not revert unrelated user changes.

Step 2: Baseline the Backend

Collect objective numbers before proposing or editing:

wc -l src-tauri/src/lib.rs 2>/dev/null
find src-tauri/src -maxdepth 3 -type f -name "*.rs" -print0 | xargs -0 wc -l | sort -n | tail
rg -n "#\\[tauri::command\\]" src-tauri/src | wc -l
rg -n "generate_handler!|invoke\\(" src-tauri/src src 2>/dev/null

Report:

  • lib.rs line count.
  • Largest Rust files.
  • Number of Tauri commands.
  • Whether command registration lives inside startup code.
  • Whether frontend uses a centralized invoke adapter.
  • Known long IPC streams or background tasks.

Step 3: Classify the Problem

Use these buckets:

Bucket Symptom Preferred fix
Dev loop Rust edits restart the app during UI work add no-watch dev script
God module lib.rs has thousands of lines split into real Rust modules
Command sprawl 100+ command functions consolidate by domain with typed routers
Invoke sprawl raw command strings everywhere frontend invoke adapter / API layer
Long IPC stale callback warnings after reload stream id + cancel + send-error stop
Restart pain route/session lost after app restart dev resume state + flush pending saves
Compile pain small domain edits rebuild huge surfaces move volatile logic out of Tauri shell

Step 4: Improve the Dev Loop

Add scripts without removing the existing full dev command:

{
  "scripts": {
    "dev:web": "vite",
    "dev:app": "tauri dev",
    "dev:app:no-watch": "tauri dev --no-watch"
  }
}

Document usage:

  • dev:app: full Tauri development with Rust watcher.
  • dev:app:no-watch: frontend-focused development; Vite HMR stays active and Rust file changes do not restart the app.
  • dev:web: pure web frontend when the app supports browser-only work.

Keep the existing pnpm tauri dev route working unless the user explicitly asks to remove it.

Step 5: Modularize the Rust Backend

Goal: src-tauri/src/lib.rs should be a thin entrypoint.

Target shape:

mod app;
mod diagnostics;
mod pty_manager;

pub use app::run;

Recommended backend shape:

src-tauri/src/
├── lib.rs
├── app/
│   ├── mod.rs
│   ├── run.rs
│   ├── command_registry.rs
│   ├── command_routers.rs
│   ├── settings_core.rs
│   ├── session_cache.rs
│   ├── session_listing.rs
│   └── ...
└── shared_runtime_modules.rs

Rules:

  • Prefer real mod modules over long-term include!.
  • During large migrations, a temporary include! split is acceptable only as a compile-preserving checkpoint. Convert it to real modules before finishing.
  • Keep each file under roughly 1000 lines unless the user sets a stricter threshold.
  • Move generate_handler! out of run.rs into command_registry.rs.
  • Keep run.rs focused on app lifecycle, plugins, menus, windows, and watchers.

Step 6: Reduce Commands Without Destroying the Boundary

Do not replace 200 commands with do_anything(action, payload). That removes type and permission boundaries.

Good consolidation pattern:

#[tauri::command]
async fn settings_command(action: String, payload: serde_json::Value) -> Result<serde_json::Value, String> {
    match action.as_str() {
        "patch_settings" => { /* typed deserialize + validate */ }
        "get_settings" => { /* typed return */ }
        _ => Err(format!("unknown settings action: {action}")),
    }
}

Better when possible:

#[derive(Deserialize)]
#[serde(rename_all = "camelCase", tag = "kind")]
enum SettingsPatch {
    SetEnv { key: String, value: String },
    DeleteEnv { key: String },
    TogglePlugin { plugin_id: String, enabled: bool },
}

Recommended domains to consolidate:

Domain Typical router
templates / marketplace install template_command
file utilities file_command
PTY lifecycle pty_command
workspace state workspace_command
MaaS/provider settings maas_command
settings/env/permissions/plugins patch_settings or settings_command

Keep explicit commands for:

  • High-risk operations where an explicit name improves auditability.
  • Commands with distinct permission or capability requirements.
  • Public app API surfaces that should remain stable.

Target: under 100 commands for medium-sized apps, lower only if domain routers stay typed and auditable.

Step 7: Centralize Frontend Invokes

Create one adapter such as src/lib/tauri.ts:

import { invoke as tauriInvoke, type InvokeArgs, type InvokeOptions } from "@tauri-apps/api/core";

const COMMAND_ROUTES: Record<string, { command: string; action: string }> = {
  install_skill_template: { command: "template_command", action: "install_skill_template" },
  pty_write: { command: "pty_command", action: "pty_write" },
};

export function invoke<T>(cmd: string, args?: InvokeArgs, options?: InvokeOptions): Promise<T> {
  const route = COMMAND_ROUTES[cmd];
  if (!route) return tauriInvoke<T>(cmd, args, options);
  return tauriInvoke<T>(route.command, { action: route.action, payload: args ?? {} }, options);
}

Then replace component imports from @tauri-apps/api/core with the app adapter.

Use TanStack Query or a local query wrapper for server-state reads. Keep imperative side effects imperative.

Step 8: Fix Long IPC and Reload Warnings

When a command streams via Channel, use a request id and cancellation:

Frontend:

const streamId = crypto.randomUUID();
const channel = new Channel<Event>();
invoke("list_items_streamed", { streamId, onEvent: channel });

return () => {
  invoke("cancel_items_stream", { streamId }).catch(() => {});
};

Also cancel on HMR / pagehide:

if (import.meta.hot) {
  import.meta.hot.dispose(() => cancelStream("hmr-dispose"));
}
window.addEventListener("pagehide", () => cancelStream("pagehide"));

Rust:

static STREAM_CANCELS: LazyLock<Mutex<HashMap<String, Arc<AtomicBool>>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

Rules:

  • invoke should resolve quickly after starting a background stream.
  • Every stream has a streamId.
  • Cleanup removes the stream token.
  • Sending to a dead channel must stop the loop.
  • HMR / route unmount / pagehide should cancel active streams.
  • Do not hide stale callback warnings globally with console.warn = ....

Step 9: Restore State After Dev Restarts

Make restarts less disruptive:

  • Persist current hash in development.
  • Persist active workspace conversation/session id.
  • Flush debounced workspace saves on pagehide, visibilitychange, and HMR dispose.
  • On reload, if the app lands on an empty/default hash, restore the last hash.
  • For expensive session lists, keep a memory/query cache and re-stream in the background.

Keep this dev-oriented unless the product requires production resume semantics.

Step 10: Verify

Run only checks allowed by local instructions:

cargo fmt
CARGO_TARGET_DIR=target/codex-check cargo check
pnpm exec tsc --noEmit --pretty false

Use a separate CARGO_TARGET_DIR when the user's tauri dev process holds the normal Cargo build lock.

Report:

  • lib.rs line count.
  • Largest Rust file count.
  • Tauri command count.
  • Rust check result.
  • TypeScript check result, including unrelated existing blockers.
  • Scripts added and how to use them.

Output Checklist

Final answer must include:

  • What changed in dev scripts.
  • What changed in backend module/command structure.
  • What changed in IPC/restart lifecycle.
  • Exact verification commands run.
  • Any remaining warnings or blocked checks.

Runtime context (shared)

Before running, read this Skill package's skill.yaml, with skill-runtime/v1 context provided by the host. Field resolution order is: current request, project context, personal Preferences, brand Profile, general defaults.

  • Use only fields declared in the Manifest; Profile stores public brand facts, Preferences stores personal work preferences.
  • required: true When a field is missing, ask the user one focused question according to the Manifest's question configuration; save the answer only after the user explicitly agrees.
  • Errors provide a copyable context_id, field path, and source; diagnostic content avoids secrets, full private paths, and raw configuration.

General feedback loop

When the user raises modification feedback during a Skill-driven task, before continuing with the current artifact you must perform:

  1. First determine whether the feedback is task-specific(this time only) or reusable(reusable across tasks).
  2. task-specific Modify only the current task, do not change the Skill.
  3. reusable First determine the scope: for domain rules, first update the corresponding canonical Skill; for rules that apply to all Skills, first update the shared specification.
  4. After completing rule updates, versioning, lint, and distribution verification, then apply the modification to the current task.
  5. reusable A modification invalidates previous "confirm," "continue," and "send it" responses; after completing the current artifact modification and read-back, you must stop and wait for the user's next instruction, without automatically proceeding to publishing, committing, or other external writes.