alien-signals vs Reflex Quant: Benchmark and Design

Two small, framework-agnostic signals runtimes. One is known for raw speed. The other was derived from a written contract and then measured. They agree on the API and differ in what they consider part of the job.

alien-signals is a fast, widely used signals library. Reflex Quant treats it the way its architecture rules treat every other runtime: as a source of counterexamples and workloads, not of obligations. It is benchmarked against alien-signals 3.2.1 on every CI run, and before any timing both must produce the same answers on every scenario.

At a glance

Reflex Quantalien-signals 3.2.1
Reactive primitivessignal, computed, effectsignal, computed, effect, effectScope
Reading and writingfunctions over handles: get, peek, set, updatethe signal is a function: call it to read, call it with a value to write
Previous value in a computedcomputed((prev) => …, initial): the last committed valuesee its documentation
Manual invalidationnonetrigger
Reordered reads (256 sources)0 relations created or removed per pass255 relinked per pass
Depth of a chainbounded by memory: one explicit work stack, no recursionsee its documentation
Specified bya written contract, an executable spec, differential testsits test suite
Size (ESM, gzip)about 2.2 KBsee bundlephobia

The structural difference: a reorder

When a computation reads the same sources in a different order, something has to happen to its dependency list. alien-signals unlinks and relinks on every out-of-place read. Quant keeps the relation and moves it.

256 dependencies, rotated every pass
edges creatededges removed
Reflex Quant00
alien-signals 3.2.1255255
Same dependency set. Different structural work. Reproduce: node bench/structure.mjs.

This is counted, not timed, so it does not depend on a machine. It is also where the timing gap is widest: on the three reorder scenarios Quant takes 0.15 to 0.17 of alien-signals' time, and on a shared graph whose read order changes every pass, 0.22. How the old relation is found without a hash table: dynamic dependency tracking.

Writes inside an effect: two findings

The Fireflies model was ported to six runtimes on a fixed 64 × 40 grid and checked against a hand-written worklist after every tick. It is a hard model for a reactive runtime: a firefly that flashes writes into its neighbours' inboxes from inside its own transition, and some of those writes land on a computation that is still running. The code, the ports and the commands are in bench/fireflies.

A wake-up is lost. Quant, Preact Signals, Vue, Solid and Angular's signal primitives end every one of 1,500 ticks in the same state as the worklist. The direct alien-signals port differs on the first tick: fireflies are left with unread pulses. The case reduces to two effects. A writes into B's inbox; B's effect runs inside that write and writes back into A's inbox while A is still running.

Case 01 Lost wake-up trace logged from lost-wakeup.mjs, the same two effects on both runtimes
  1. alien-signals 3.2.1
  2. A runs
  3. A writes B's inbox
  4. B runs inside A's write depth 2
  5. B writes A's inbox
  6. A is still running: not run again
  7. A's write returns
  8. A ends
  9. 0.2 left unread in A's inbox
  1. Reflex Quant 1.1.2
  2. A runs
  3. A writes B's inbox: B is queued
  4. A's write returns
  5. A ends
  6. B runs depth 1
  7. B writes A's inbox: A is queued
  8. A runs again, reads 0.2
  9. inbox empty

In alien-signals the effect is not run again for a dependency that was written during its own run. Preact Signals delivers the pulse too. Quant never reaches that position: an effect does not run inside another effect's write. Effects run one at a time from the host's queue, so B runs after A has returned, and B's write wakes A like any other write. If a write does land on an effect while it runs, the contract covers that case as well (W2: a watcher invalidated during its own run is delivered again). Whether alien-signals intends its behaviour is a question for its maintainers; the test only shows what it does. To get the right answer the port needs a repair written by hand: after every tick, re-trigger each firefly that still has mail, until none does.

An avalanche lives on the call stack. In alien-signals a write inside an effect runs the dependent effect inside that write, so a cascade nests effects in one another. On this model the nesting reaches 2,560, the whole field. On Node's default stack the repaired port overflowed at tick 262 with 2,457 effects nested in one run and completed in another: at this size it sits on the limit. In Quant the trace above stays at depth 1 however long the cascade: effects run from a queue, and propagation runs on the runtime's own work stack, bounded by memory.

The same ports on a 200 × 200 field, 40,000 fireflies, 900 ticks, default stack size:

result
Reflex Quantsame state as the worklist; all 40,000 flash in one tick; 5.3 s
Angular signal primitivessame state as the worklist; 7.6 s
Solidsame state as the worklist; 44.7 s
Preact SignalsCycle detected at tick 167, its limit on repeated effect runs
Vue reactivitycall stack overflow at tick 341
alien-signals, repaired portcall stack overflow at tick 247

One model, one machine (Node 25.2.0), each port one reasonable way to write it. This is a probe of semantics and stack behaviour, not a ranking; the timings are in the folder's README.

Where alien-signals is faster

On the shortest paths. Reading a clean value (static.read-clean) costs Quant 1.22 of alien-signals' time, and mounting effects (lifecycle.mount-100) 1.04. A read that finds nothing to do is almost all call overhead, and alien-signals has less of it. The pattern across the suite is consistent: the more a scenario looks like a graph, with shared nodes, fan-in and changing read order, the further Quant pulls ahead; the more it looks like a single function call, the closer alien-signals gets, and on the two above it wins.

Benchmarks

The suite is this project's own; lower is better. Node 22.23.4, 9 rounds; each ratio measured within one runner; Reflex bf71877, alien-signals 3.2.1. Earlier snapshots: REPORT.md (Q6), HISTORY.md.

Per-scenario chart and method: benchmarks.

What Quant leaves out on purpose

effectScope, nested-effect ownership and manual invalidation are real answers to real problems: who owns a group of effects, and how to tell the graph that a mutable container changed. They are answers to different questions from the one Quant asks. Its rule for admitting a mechanism is a named obligation and a minimal program that violates it without the mechanism. "Another runtime has it" is explicitly not a reason. So Quant has dispose for one node and no notion of a scope; ownership is left to the framework above it.

That is a cost if you want those features in the box, and the reason the runtime is small if you do not.

What Quant writes down

What a computed depends on after it throws. Whether A → B → A is a change. What happens to a computation whose source is written while it runs. Whether an obligation to run survives a failure. Quant answers each of these in its contract, executes the answers against a specification on over 140,000 programs, and removes each mechanism in turn to check that a test notices. This page makes no claim about how alien-signals behaves in those cases; only that Quant's behaviour is specified and tested there.

When to choose which

Choose alien-signals if you want effect scopes in the library, a large user base, and the fastest possible read of a clean value. Choose Reflex Quant if your graphs reorder and share, if you want behaviour under failure written down, or if you want a runtime small enough to read in an afternoon and argue with.

Try Quant

npm install reflex-quant

Evidence

FAQ

Is Reflex Quant an alternative to alien-signals?

Yes: both provide framework-agnostic signal, computed and effect. Quant is smaller in surface, reuses dependency relations on reorder, and is specified by a written contract.

Is Reflex Quant faster than alien-signals?

On the project's own 18-scenario CI suite its geometric-mean time is 0.45 of alien-signals' (CI run 42794143933). The suite is the project's own; per-scenario numbers are on the benchmarks page of this site.