# Performance & Tuning Practical reference for getting the most out of vapor-chamber. Most of what this document describes is **already done by default** — the lib is V8-aligned out of the box. The "Tuning" section below is for cases where you want to trade defaults for higher throughput on specific hot paths. --- ## Philosophy The lib targets two different optimization regimes: 1. **Read-many hot paths** — places where the same object is touched by plugins, hooks, listeners, and consumer code on every dispatch. Examples: `result.ok`, `cmd.meta.id`, `cmd.action`. These rely on **monomorphic hidden classes** so V8's inline caches stay specialized. The lib enforces shape consistency on `Command`, `CommandResult`, `CommandMeta`, and the internal bus state. 2. **Algorithmic complexity** — places where the cost scales with usage pattern. Examples: listener fan-out (O(n) walk → O(1) hash + O(w) wildcard walk), persist plugin saves (one per dispatch → one per microtask). What the lib does **not** chase: - Loop-syntax micro-opts (cached `length`, index-vs-`for...of`). V8's TurboFan handles these. The few places where index loops are kept are documented; everywhere else, idiomatic code is fine. - Property mangling, Closure ADVANCED, asm.js / Wasm. Friction far exceeds gain for a library at this size. - Premature parallelism. Hooks run sequentially because order matters; opt-in parallel is on the roadmap, not the default. --- ## What's optimized by default You get all of this without changing any code. Listed for diagnostic transparency only. ### Hot-path shape consistency - `okResult` / `errResult` always allocate `{ ok, value, error }` with the unused slot set to `undefined`. One hidden class for both, monomorphic property access at every consumer site. - `stampMeta` always allocates `{ ts, id, correlationId, causationId }` with stable field order. No late property additions, no shape transitions. - `Command` literal always `{ action, target, payload, meta }` in the same order across `dispatch` / `query` / `emit` / `request` paths. - `AsyncState` and `SyncState` initialized via single object literals at bus construction — no incremental field writes that would create shape branches. ### Pre-composed plugin chain `bus.use(plugin)` rebuilds a single composed `runner` function once per plugin add/remove. Dispatch calls `runner(cmd, execute)` directly — no per-dispatch chain walk, no per-dispatch closure allocation. ### Listener bucketing `bus.on('cartAdd', fn)` (exact match) goes into a `Map` for O(1) lookup at dispatch time. `bus.on('cart*', fn)` (wildcard) goes into a separate array walked with `matchesPattern` only when wildcards exist. Real-world impact (5k dispatches × 55 listeners): - dispatch: +12% (415 → 466 ops/sec) - emit: +26% (403 → 507 ops/sec) Scales with listener count: silent for <5 listeners, larger beyond ~50. ### Counter-based `meta.id` The default unique-ID generator is a per-process random prefix + monotonic counter (~30–50 ns per call). Was `crypto.randomUUID()` (~1–2 µs). Measured 2.26× speedup on the 10k-dispatch hot path. If you need cryptographically unique IDs (distributed tracing, cross-process auditing), opt in: ```ts import { configureUid } from 'vapor-chamber'; configureUid(() => crypto.randomUUID()); ``` Call once at app setup. Affects all subsequent dispatches. ### Wildcard pattern prefix cache `matchesPattern('foo*', 'fooBar')` caches the prefix (`'foo'`) per pattern in a 256-entry LRU. Avoids `String.prototype.slice()` on every match. ### Tree-shake-friendly imports The signal API lives in a side-effect-free `src/signal.ts` module. Importing `createHttpBridge` or `createFormBus` does not pull in the Vue feature-detection registry from `chamber.ts`. Typical Blade-style consumer bundle (`createCommandBus` + `createHttpBridge` + `logger`): | | Bundle | |--|--| | Raw | 17 KB | | Brotli | **5.7 KB** | | Vapor probing references | 0 | Composables (`useCommand`, `useVaporCommand`, etc.) only land in your bundle if you explicitly import them. --- ## Tuning knobs (consumer-facing) ### `persist({ ..., coalesce: true })` — collapse rapid saves By default, every successful dispatch with the persist plugin does one `getState()` + `JSON.stringify()` + `setItem()` cycle. For workloads where many rapid commands touch the same state (form input, scroll tracking, batched cart updates), enable coalescing: ```ts import { persist } from 'vapor-chamber'; bus.use(persist({ key: 'vc:cart', getState: () => cart.value, coalesce: true, // <— save once per microtask burst, not per dispatch })); ``` Trade-off: 1 microtask of latency before the save lands. Storage reads immediately after a burst of dispatches will see the pre-burst state until the next tick. Measured on 100 rapid dispatches × 50-item array state: - Default: 3,300 ops/sec - `coalesce: true`: 28,887 ops/sec (**8.75×**) Use when you're measurably bottlenecked on persist; leave default otherwise to keep storage in lockstep with bus state. ### `configureUid(fn)` — swap the unique-ID generator Default: counter + per-process random prefix. Fast, in-process unique. Opt in to `crypto.randomUUID()` if you ship command IDs to a distributed tracing backend or use them as cross-process correlation keys: ```ts import { configureUid } from 'vapor-chamber'; configureUid(() => crypto.randomUUID()); ``` Call once at app setup, before any dispatches. ### `useSharedCommandState()` — one set of signals shared across many components Default composables (`useCommand` / `useVaporCommand`) allocate two reactive signals (`loading`, `lastError`) **per call**. On a page with 50 components each calling one of them, that's 100 signal nodes in the reactivity graph. Most of those components only need to know "is *anything* in flight?" — they don't need their own private loading state. `useSharedCommandState()` returns the **same** signal instances to every caller subscribed to the same bus. State is per-bus (multiple buses → multiple shared states), ref-counted (auto-dropped when the last subscriber disposes), and exposes a ring-buffered errors list capped at `errorCap` (default 10). ```ts import { useSharedCommandState } from 'vapor-chamber'; // In any number of components — all see the same isAnyLoading / errors / lastError. const { dispatch, isAnyLoading, lastError, errors, errorCount, clear } = useSharedCommandState(); // Bind to button disabled across the whole UI: // // // Show a top-of-page error toast: // {{ lastError.value.message }} await dispatch('cartAdd', product); ``` Behavior: - **`inFlight`** counts concurrent dispatches across all subscribers; auto-decrements when each completes (sync throw, async reject, async ok=false, all paths). - **`isAnyLoading`** is `true` when `inFlight > 0`. - **`errors`** is a ring buffer (newest last); older entries drop when length exceeds `errorCap`. - **`lastError`** is the most recent error. - **`clear()`** wipes errors / lastError; does not affect `inFlight`. - **`{ signal }`** option forwards to the underlying bus dispatch (the v1.2.x AbortController integration), so cancellation works the same as `useCommand`. - **Auto-cleanup** via `tryAutoCleanup` — Vue scope/component disposal calls `dispose()` automatically. When to use: - **Use `useSharedCommandState`** when many components only need aggregate state ("any loading?", "any errors?"). Toolbars, status bars, global spinners, error toast lists. - **Use `useCommand`** when a component needs its own private loading/error scoped to its own button or form. Component-local UI state. Both can coexist on the same bus. ### `dispatch(..., { signal })` — cancelable async dispatch Pass an `AbortSignal` as the 4th argument to cancel an in-flight async dispatch: ```ts import { createAsyncCommandBus } from 'vapor-chamber'; const bus = createAsyncCommandBus(); bus.register('searchProducts', async (cmd) => { // Handler can observe cmd.signal mid-flight return await fetch('/api/search?q=' + cmd.target, { signal: cmd.signal }); }); const ac = new AbortController(); const result = bus.dispatch('searchProducts', 'denim', undefined, { signal: ac.signal }); // Later — user types a new query, abort the in-flight search ac.abort(); ``` Behavior: - **Pre-aborted signal** → resolves immediately with `{ ok: false, error }`, handler is **not** called. The error is the explicit reason (`ac.abort(myError)`) if provided, otherwise a `BusError` with `code === 'VC_CORE_ABORTED'`. - **Mid-flight abort** → handler observes `cmd.signal.aborted === true`. The handler is responsible for stopping its own work — the bus does not forcibly terminate it. - **HTTP bridge** auto-forwards `cmd.signal` to `fetch`. No need to thread the signal through `createHttpBridge` options at construction. - **After-hooks fire** for aborted dispatches so loggers and metrics see the cancellation. - **Sync bus** accepts `{ signal }` for type uniformity but ignores it at runtime — sync dispatches are atomic. **Not yet supported** (deferred to v1.3): `bus.request()` / `respond()`, `bus.dispatchBatch()`, auto-derived child signals from parent dispatches, WebSocket / SSE bridges. Use `cmd.signal` directly in custom handlers / plugins as a workaround. ### `vapor-chamber/alien-signals` — push-pull reactivity for non-Vue consumers Vue 3.6's `ref()` is itself a port of [alien-signals](https://github.com/stackblitz/alien-signals) ([vuejs/core#12349](https://github.com/vuejs/core/pull/12349)) — so when vapor-chamber auto-detects `vue.ref` you're already on alien-signals' algorithm under the hood. For **non-Vue contexts** — SSR/Node services, Web Workers, embedded widgets, anywhere you want push-pull reactivity without Vue's full runtime — the `vapor-chamber/alien-signals` connector flips vapor-chamber's underlying signal factory in one call: ```ts import { signal as alienSignal } from 'alien-signals'; import { configureAlienSignals } from 'vapor-chamber/alien-signals'; configureAlienSignals(alienSignal); // From here on, every vapor-chamber signal() — including useCommand, // useSharedCommandState, FormBus signals — is backed by alien-signals' // push-pull propagation algorithm. computed() / effect() from alien-signals // observe the same underlying instances. ``` **Implementation note:** the connector takes alien-signals' `signal` function as an argument rather than importing it. vapor-chamber stays free of an `alien-signals` runtime dep; consumers install it themselves (~7.5 KB raw / ~2.5 KB brotli). 7 tests in [tests/alien-signals.test.ts](../tests/alien-signals.test.ts) verify the adapter against the real published package, not a stub. ### `configureSignal(fn)` — provide your own signal implementation The lib auto-detects Vue's `ref()` for reactivity. If you're using a custom signal library or want to wire alien-signals directly: ```ts import { configureSignal } from 'vapor-chamber'; import { signal as alienSignal } from 'alien-signals'; configureSignal((initial) => { const s = alienSignal(initial); return { get value() { return s(); }, set value(v) { s(v); } }; }); ``` Useful for SSR / non-Vue environments where you still want reactive bus state. --- ## Choosing an IIFE variant For `