Performance
Quantum simulation is computationally expensive. State space grows exponentially with the number of qudits. This page covers how to keep your game running at 60 FPS.
The One Rule: Pool Your Properties
Property pooling is the single most important optimization. Without pooling, every new property grows the tensor product. With pooling, measured properties are reset and reused within the existing shared state.
// After measurement, ALWAYS release to pool
const [value] = this.getModule().measure_properties([prop]);
this.deleteProperty(id);
this.releaseProperty(prop, value); // reset to |0⟩, return to poolQuantumPropertyManager handles this automatically when you use removeProperty(id), which measures, deletes, and releases in one call.
Why Pooling Works
When you acquireProperty():
- Pool is not empty → reuses a property already in the shared state. No tensor product growth. Fast.
- Pool is empty → creates a fresh property. If this property later
i_swaps with an existing one, the WASM module computes a tensor product to merge the state spaces. Expensive.
Pooled properties are already in the shared state, so entangling them (via i_swap) is cheap.
Shipped Limits
The published package ships two editions with different trade-offs:
| Edition | Max Dimension | Max Qudits | State Space per Qudit |
|---|---|---|---|
| Qutrit (default) | 3 | 12 | 2–3 basis states |
| Qubit | 2 | 20 | 2 basis states |
Choose your edition during npx quantum-forge init (or with --edition qubit). The Qutrit edition gives you three-valued properties but fewer total qudits. The Qubit edition trades away qutrits for a higher qudit ceiling. See Quantum Setup: Editions.
Properties in separate (non-entangled) systems don't count against each other.
Performance Budget
60 FPS target guidelines (Qutrit edition: dimension ≤ 3, 12 max qudits; Qubit edition: dimension 2, 20 max qubits):
| Active Properties | Performance | Notes |
|---|---|---|
| 1–4 | No issues | Comfortable headroom |
| 4–8 | Good | Monitor frame times |
| 8–10 | Watch carefully | Only with good pooling |
| 10–12 | At the limit | Aggressive pooling required |
Expensive Operations
Ranked by cost:
Tensor product: triggered by
i_swapbetween properties in different shared states. Cost grows exponentially with the number of qudits in each state. This is the operation to minimize.Fresh property creation: when the pool is empty. The property itself is cheap, but the first
i_swapwith an existing property triggers a tensor product.Measurement: proportional to state vector size. Fast for small systems, noticeable for 15+ entangled qudits.
Gate application: fast. Scales linearly with state vector size.
Probability query: similar cost to measurement but no state change.
Optimization Strategies
1. Aggressive Pooling
Measure and release as soon as a property is no longer needed:
// Ball exits field → measure immediately, don't wait
const exists = registry.measureExistence(ballId);
// Property is now back in the pool2. Limit Concurrent Quantum Objects
Design your game so that only a few objects are quantum at any time:
- Quantum Pong: max 4-6 balls quantum at once (2-3 pairs)
- Quantris: current piece + at most 2-3 recently placed pieces
- Bloch Invaders: only the targeted invader is actively quantum
3. Choose the Right Dimension
Higher dimensions = exponentially larger state space:
| Dimension | States per property | Memory per entangled pair |
|---|---|---|
| 2 (qubit) | 2 | 64 bytes |
| 3 (qutrit) | 3 | 144 bytes |
The Qutrit edition supports dimensions 2 and 3. The Qubit edition is locked to dimension 2 but supports up to 20 qubits. If you only need binary states and want more quantum objects, consider the Qubit edition.
4. Separate Registries for Independent Systems
If your game has quantum objects that never interact, use separate registries:
const playerRegistry = new PlayerQuantumRegistry(logger); // player abilities
const enemyRegistry = new EnemyQuantumRegistry(logger); // enemy states
// These systems never entangle with each other,
// so they maintain independent (small) state spaces5. Batch Operations
Batch gates: When applying many gates per frame (AI sweeps, circuit replay), use executeBatch() to send them all in one WASM call. This eliminates per-call JS-WASM boundary overhead (~1-2 us per crossing), which dominates when gate math itself is fast (~1-2 us per gate).
import type { BatchOp } from "quantum-forge/quantum";
const ops: BatchOp[] = tape.map(instr => ({
op: instr.gate,
target: instr.property,
predicates: instr.controls,
}));
const result = m.executeBatch(ops); // one WASM call instead of ~100See Gates: Batch Gate Execution for the full API.
For maximum throughput, use executeBatchTape() with a pre-encoded Float64Array — see Gates: Tape-based batch.
Batch measurement: Measure multiple properties at once instead of one at a time:
const values = this.getModule().measure_properties([prop1, prop2, prop3]);6. Throttle Probability Queries
Reading probabilities for visualization doesn't need to happen every frame:
private probCache = new Map<string, number>();
private lastProbUpdate = 0;
getExistenceProbability(id: string): number {
const now = performance.now();
if (now - this.lastProbUpdate < 100) { // update at most 10x/sec
return this.probCache.get(id) ?? 1.0;
}
this.lastProbUpdate = now;
const prop = this.getProperty(id);
if (!prop) return 1.0;
const prob = /* query probabilities */;
this.probCache.set(id, prob);
return prob;
}Destroying Unused Properties
When a property is no longer needed but you don't need its measurement value, call destroy() to remove it from the shared state vector entirely:
m.destroy(tempProp); // factorizes qudit out of state vectorThis is more aggressive than pooling — it shrinks the state vector rather than keeping the qudit in |0⟩. Use it for temporary properties in replay/adapter scenarios. See Lifecycle & State Management for details.
Isolated Simulations
For search tree exploration or replay branches, use QuantumSimulation to create isolated quantum contexts. Each simulation has its own state vectors, preventing cross-contamination:
const sim = QuantumForge.createSimulation();
const prop = sim.createProperty(2);
// ... explore this branch ...
sim.destroy(); // releases everything at onceThis prevents the common problem of replay branches growing a shared global state vector. See Lifecycle: QuantumSimulation.
Monitoring
WASM heap usage
Use getWasmMemoryBytes() to monitor WASM heap usage:
import { getWasmMemoryBytes } from "quantum-forge/quantum";
// In your debug overlay
const memBytes = getWasmMemoryBytes();
if (memBytes !== null) {
debugText(`WASM: ${(memBytes / 1024).toFixed(0)} KB`);
}State budget queries
Query the state vector size for any property to monitor quantum state complexity:
const sparseSize = m.state_vector_size(prop);
const numQudits = m.num_active_qudits(prop);
debugText(`State: ${sparseSize} amplitudes, ${numQudits} qudits`);Use getMaxStateSize() to compare against the hard limit (100,000 basis amplitudes). See Lifecycle: State Budget for backpressure patterns.
Error Handling
WASM operations throw typed JavaScript errors when they fail. The most important for performance is [QuantumForgeStateSizeError], which fires when a tensor product would exceed the state size limit:
quantumSplit(originalId: string, newId: string): boolean {
const prop1 = this.getProperty(originalId);
if (!prop1) return false;
const prop2 = this.acquireProperty();
try {
this.getModule().i_swap(prop1, prop2, 0.5);
} catch (e) {
if (e instanceof Error && e.message.startsWith("[QuantumForgeStateSizeError]")) {
this.logger?.warn?.("State too large for split, falling back to classical");
} else {
this.logger?.warn?.("Quantum operation failed, falling back to classical");
}
this.releaseProperty(prop2, 0);
return false;
}
this.setProperty(newId, prop2);
return true;
}The OOM guard estimates tensor product size before allocating, so the operation fails cleanly without corrupting state. Design your game so that hitting the limit is graceful, not fatal. See Error Handling for the full guide.