Skip to content
Unverified: This page has not yet been anchored to automated code tests.

vox-gui Performance Conventions

Adopted: 2026-06-14. Covers the vox-gui Tauri frontend (crates/vox-gui/ui/).

RAIL is Google’s model for user-centric performance. The following budgets apply to all vox-gui surfaces.

PhaseBudgetNotes
Response (user action → visual feedback)< 100 ms total; < 50 ms JS processingButton clicks, keyboard shortcuts, UI state toggles; aligns with INP “input delay” target
Animation (each frame)< 10 ms JS work per frameRemaining 6 ms budget is for the browser’s compositor
Idle (deferred work)≤ 50 ms chunksUse requestIdleCallback or setTimeout(fn, 0) for background parsing
Load (shell to interactive)< 1 000 ms on target hardwareTauri webview startup + JS parse + first render
INP (Interaction to Next Paint)< 200 ms (p75)Operator-console target; investigate with Chrome Performance panel when exceeded

50 ms is the “Long Task” threshold (Chrome DevTools / LoAF). Any JS that blocks the main thread for > 50 ms is flagged as a long task and produces jank at 60 fps.


2. Animation Rule: transform and opacity Only

Section titled “2. Animation Rule: transform and opacity Only”

Rule: Animate only transform and opacity (and filter when unavoidable). Never animate width, height, top, left, margin, padding, or background-color via JS.

Why: transform and opacity are composited by the GPU on a separate layer. Animating them skips layout and paint — the two most expensive browser pipeline stages. Animating geometry properties triggers layout on every frame, burning through the 10 ms frame budget instantly.

How to apply in Tailwind:

/* Good — compositor only */
.slide-in { animation: slideIn 200ms ease-out; }
@keyframes slideIn { from { transform: translateY(8px); opacity: 0; } to { transform: none; opacity: 1; } }
/* Bad — triggers layout every frame */
.expand { animation: expand 300ms ease; }
@keyframes expand { from { height: 0; } to { height: 200px; } }

Existing keyframes in src/index.css: vox-toast-in, vox-ping, vox-pulse-slow all use transform/opacity/scale — compliant. Add new keyframes only with transform/opacity.

prefers-reduced-motion: Handled in Phase 0C. Do not duplicate here.


Rule: Any computation that is not trivially O(1) or O(n) with n < 50 must be moved to a Tauri #[command] in Rust.

CategoryThresholdAction
Data filtering / sorting> 500 itemsRust command
String search / regex over corpusany corpus sizeRust command (via vox_search_query)
Cryptographic operationsanyRust command
File I/O or networkanyRust command (Tauri already enforces this)
JSON parsing of large payloads> 100 KBConsider streaming from Rust
Embedding / ML inferenceanyRust command (via MENS pipeline)
// BAD — blocks the JS thread for large data sets
const filtered = hugeArray.filter(item => item.score > threshold);
// GOOD — offload to Rust, return the processed result
const filtered = await invoke<Item[]>('filter_items_by_score', { threshold });

Rule: Any list that can exceed 50 items must be virtualized using useVirtualList (src/hooks/useVirtualList.ts).

SurfaceListStatus
TasksViewinProgress, queuedVirtualized (Phase 0D)
MemoryViewrecent_recallsVirtualized (Phase 0D)
MemoryViewshardsVirtualized by row (Phase 0D)
RunsViewrunsHard-bounded at 40 — no virtualization needed
SearchViewsearch hitsHard-bounded at 30 — no virtualization needed

The project intentionally uses style={{}} for dynamic values that cannot be expressed as static Tailwind classes. Examples:

  • style={{ width: '${score * 100}%' }} — progress bars
  • style={{ height: totalSize, position: 'relative' }} — virtualizer inner div
  • style={{ transform: \translateY(${vItem.start}px)` }}` — virtual item position

These are not style violations. The CSP already includes style-src 'unsafe-inline' to allow them.