DEV Community

Cover image for Inside a 2.2 KB Signals Runtime: One Graph, One Stack, No Recursion
Andrii Volynets
Andrii Volynets

Posted on Originally published at volynetstyle.github.io

Inside a 2.2 KB Signals Runtime: One Graph, One Stack, No Recursion

A reactive runtime begins with a simple idea: record dependencies when values are read, then notify consumers when those values change. The difficulty begins when dependencies change order, computations fail halfway through execution, or writes invalidate computations that are still running. Each case invites another flag, index, queue, or special rule. Eventually, much of the runtime exists to coordinate its own bookkeeping.

Reflex Quant began with a different question: What is the smallest machine we can derive from a fixed reactive contract, without inheriting the architecture of an existing implementation?

The result is a JavaScript signals runtime of approximately 2.2 KB gzip. It supports exact dynamic dependencies, glitch-free propagation, computed values that retain state between executions, defined failure recovery, and graph traversal without recursion. Its size is interesting, but the more important result is how many apparently separate mechanisms can share the same representation.

One relation, several responsibilities

Quant separates its implementation into a kernel, a host, and a public API. The kernel owns dependencies and change propagation. The host delivers effects, manages cleanup, and implements batching. The public API exposes these operations without adding another layer of graph objects.

Inside the kernel, the graph has just two object types: Node and Edge.

Every dependency is represented by an edge that belongs to two linked lists simultaneously. Its consumer keeps the edge in dependency-read order, while its source keeps the same object in subscriber-delivery order.

X.deps:   [A → X] ↔ [B → X] ↔ [C → X]
                       │
                       │ same Edge
                       │
B.subs:   [B → W] ↔ [B → X] ↔ [B → Y]
Enter fullscreen mode Exit fullscreen mode

The consumer's list supports tracking and validation. The source's list supports invalidation. There is no second representation to synchronize because both lists contain the same relation.

Each edge also records the source version its consumer observed and the generation in which the relation was confirmed. These fields allow the graph to remember not only who depends on whom, but also what was observed and whether that observation was successfully committed.

This matters when dependencies change dynamically.

A computation that reads A, B, C on one execution might read B, C, A on the next. The dependency set has not changed, so destroying and recreating its edges would be unnecessary. Quant instead retains the relations and changes their order.

On a stable execution, a cursor walks the existing dependency list. Each matching read advances the cursor without allocation or lookup. When the order differs, Quant searches for the existing relation using progressively more expensive exact methods: immediate structural checks, bounded scans, and finally temporary indexing or marking.

The scan limit is a cost limit, not a correctness shortcut. Failing to find an edge within eight steps never proves that the edge does not exist. When necessary, the runtime pays for one linear preparation pass rather than repeatedly searching the same graph.

In the reorder.rotate-256 structural test, Quant creates and removes no relations per pass. The tested alien-signals 3.2.1 implementation relinks 255 per pass. These are counted graph operations, not benchmark timings, and they illustrate a difference in representation rather than an automatic victory in execution speed.

The deeper principle is that an existing relation should not become a new object merely because the computation encountered it in a different order.

Change is evidence, not merely a new value

Quant uses push-pull propagation. Writes propagate invalidation, while reads validate upstream values and execute computations only when necessary.

The distinction matters in a diamond-shaped graph, where two computations depend on one source and a third depends on both. Eagerly recomputing the third after only one branch has updated could expose inconsistent intermediate state.

Quant distinguishes a known need to execute, represented by DIRTY, from possible upstream changes, represented by STALE. Validation resolves that uncertainty before execution. If a computed produces an Object.is-equal result, its version does not change, and the update stops propagating.

Versions also preserve information that value comparisons alone would lose. If a signal changes from A to B and back to A, its value has returned to the starting point, but two committed changes occurred. The consumer's edge still records the earlier version, so the outstanding observation cannot silently disappear.

The same mechanism handles a less obvious case: a computation reads a source, changes it, and reads it again during the same execution.

const a = signal(1);

const c = computed(() => {
  const first = get(a);
  if (first === 1) set(a, 2);
  return first + get(a);
});

get(c); // 3
get(c); // 4
get(c); // 4
Enter fullscreen mode Exit fullscreen mode

The first execution combines the initial observation of 1 with the later value 2. Its result is 3, but that execution cannot be considered settled. The source changed after it was first observed, so another execution is owed.

Quant preserves the version of the first observation for that execution. Reading the source again does not erase the evidence of the intervening change.

This is a semantic obligation, not an optional scheduling preference. The runtime must remember that an execution can finish successfully while still owing another execution.

Failure must not rewrite history

Exceptions create a related problem. Suppose a computed successfully reads A, B, C, then fails during another execution after reading A, D.

Discarding the old dependencies would be incorrect because B or C might change and allow the computation to recover. Ignoring D would also be incorrect because it may be the source responsible for the failure.

Quant retains the last successful dependencies while allowing observations from a failed attempt to remain tentative.

Run 1   A B C   success   A B C committed
Run 2   A D     throws    A B C + D tentative
Run 3   A E     throws    A B C + E tentative
Enter fullscreen mode Exit fullscreen mode

An edge created during execution receives the next generation number. A successful execution advances the node's generation, committing the newly confirmed relations together. A failed execution does not advance it.

The runtime therefore distinguishes committed history from temporary observations without maintaining a separate rollback log. Repeated failures also do not accumulate an unlimited collection of tentative edges.

Effects impose a further constraint. Their cleanup functions may perform irreversible user operations, so Quant validates an effect's entire committed dependency set before running its previous cleanup. A failed validation must not destroy a still-valid lifecycle.

The graph and its version history provide the evidence, while the host controls the lifecycle consequences.

One stack for three different problems

Three operations appear to require separate execution machinery.

Invalidation must traverse downstream subscribers. Validation must descend into upstream dependencies and return. Nested tracking must preserve temporary source slots overwritten by an inner computation.

Quant implements all three using one explicit stack of existing edges.

During propagation, an edge identifies the next branch to visit. During validation, it identifies the return path. During nested tracking, it preserves a displaced indexing slot.

The last case reveals why storing an edge is sufficient. Every edge already contains a reference to its source through edge.from. Saving both the displaced edge and its source would duplicate information that can be recovered directly.

Changing the tracking stack to store only edges reduced peak stack entries from 1,542 to 771 in the project's nested-index test.

More importantly, the same array now serves push, pull, and tracking. None requires recursive graph traversal, so dependency depth is limited by available memory rather than the JavaScript call stack.

This is the central architectural observation of Quant: several operations that look unrelated become simpler when their continuations are represented by the same graph object.

What the measurements show

The design has measurable trade-offs. In the project's 18-scenario CI benchmark, Quant 1.1.2 achieved the following geometric-mean time ratios on November 10, 2026:

Comparison Quant time ratio
Against alien-signals 3.2.1 0.45×
Against Reflex 0.99×
Against the faster rival in each scenario 1.03×

Lower values indicate faster execution. Quant benefits from shared dynamic graphs, branch changes, and avoiding unnecessary relation reconstruction. It does not dominate every workload. Reflex remains approximately 20–21% faster in the measured reorder scenarios, while alien-signals is faster on the clean-read microbenchmark.

A separate, broader experiment offers another perspective. The JS Reactivity Benchmark compares 30 JavaScript reactivity implementations across different workload families. Its published November 10, 2026 report covers 51 scenarios, of which 33 shared core scenarios contribute to the performance leaderboard. Three additional core scenarios were excluded from every participant's score because their evidence was incomplete.

The leading twelve entries were:

Rank Runtime Relative score
1 Reflex Quant 1.07×
2 alien-signals 1.20×
3 Reflex 1.24×
4 Reactively 1.65×
5 Preact Signals 1.67×
6 Vue Reactivity 3.6 RC 1.69×
7 Anod 1.89×
8 ReScript Signals 1.93×
9 Lite Signal 1.95×
10 Maverick Signals 2.00×
11 Vue Reactivity 3.5 2.27×
12 Angular Signals 2.43×

The score is a family-balanced geometric mean of execution time relative to the fastest implementation in each shared scenario. Each workload family receives equal weight, and lower is better. The published figures are medians across three machine sets, with comparisons formed within each machine rather than between unrelated CPUs.

Quant recorded 15 decisive scenario wins and the lowest overall core score, approximately 13% ahead of alien-signals by the report's relative-score comparison. Its score ranged from 1.03× to 1.07× across the three machine sets.

The leaderboard is deliberately narrower than the complete test suite. Thirteen large-graph stress scenarios are reported separately; Quant completed all thirteen. Two lifecycle capability scenarios are also separate, and Quant does not provide the ownership capabilities they require. Unsupported operations are not counted as fast executions, nor are they silently added to the core score.

These distinctions matter. A benchmark can compare implementations only over a defined common contract. The result does not establish that a complete Vue, Angular, or Solid application will execute in the same proportions, nor does it measure rendering performance. It describes the tested reactive adapters under specific graph workloads and measurement rules.

The two benchmark suites use different workloads and scoring methods, so their aggregate ratios should not be combined. What they independently support is a narrower architectural conclusion: Quant's compact representation remains competitive across both targeted mechanisms and a broader collection of reactive graph workloads.

What the experiment establishes

Correctness was examined separately from performance. Quant was checked against an executable specification on 27,061 bounded programs, 18,660 recovery programs, and 100,000 generated programs, with graph integrity checked after each operation.

The project also tested 34 mechanism ablations. Every removal violated at least one semantic, structural, representation, or ordering test.

This does not prove that Quant is the smallest possible reactive runtime. An ablation establishes that a mechanism is necessary to this implementation under its current representation and obligations, unless something else replaces its role. Another architecture may satisfy the same contract differently.

Nor is Quant a complete framework. Effect scopes, rendering, ownership trees, asynchronous resources, and transactional updates remain outside its kernel. Those responsibilities are not inherently unimportant; they belong to different layers of the system.

The experiment suggests a broader lesson about software architecture. We often optimize by making individual operations faster, while leaving their separate representations unquestioned. Yet many costs arise because the same fact is stored repeatedly, then synchronized across subsystems.

Quant takes the opposite approach. Dependency order, observed versions, committed history, temporary indexing, and traversal continuations are derived from a small graph whose objects carry several responsibilities.

The price is that correctness depends on strong invariants shared across those responsibilities. Fewer mechanisms do not eliminate complexity. They concentrate it where it can be specified, tested, and examined.

A runtime becomes simpler not necessarily by solving fewer problems, but by storing each truth in one place.


The interactive anatomy of Reflex Quant illustrates these mechanisms with executable examples, dependency diagrams, and a shared-stack visualization.

The complete source code, semantic contract, research report, 18-scenario CI benchmarks, and broader JS Reactivity Benchmark are public.

Reflex Quant is MIT-licensed and published on npm. It remains a research-driven implementation rather than a library with an established history of production deployment.

Top comments (0)