DEV Community

Sui Gn
Sui Gn

Posted on

How to optimize massive reactive dependency trees

Reactive systems become expensive when every mutation forces the runtime to inspect the whole graph. The [.me](https://www.npmjs.com/package/this.me) kernel avoids that pattern by treating derivations as indexed dependencies rather than as global recomputation jobs.

In [.me](https://www.npmjs.com/package/this.me), a derived value is not just a stored result. It is registered as a MEDerivationRecord with an expression, an evaluation scope, and a list of resolved reference paths. The important structure is the reverse index: refSubscribers. Instead of asking “what depends on the whole graph?”, the kernel asks “what depends on this exact path?”

For example, a derived assignment such as:

me.order.price(100);
me.order.quantity(5);
me.order["="]("total", "price * quantity");
Enter fullscreen mode Exit fullscreen mode

causes the derivation engine to extract price and quantity, resolve them relative to order, and register order.total as a subscriber of those reference paths. This happens in registerDerivation(): each resolved ref gets a Set of target derivations.

On write, the runtime calls invalidateFromPath(). The changed path gets a new ref version, then the kernel looks up only refSubscribers[changedPath]. In eager mode, those targets are recomputed immediately and pushed into the queue, allowing chained derivations to propagate one edge at a time. In lazy mode, the write only bumps versions; readPath() and explain() call ensureTargetFresh() when a stale derived target is read.

How to Optimize Massive Reactive Dependency Trees

The practical complexity is therefore O(k), where k is the affected dependency frontier, not N, the total number of paths in the graph. This distinction matters. In the City_Scale demo, N = 10,000 districts exist, but mutating districts[5001].currentLoad recomputes only:

districts.5001.loadPercent
districts.5001.overCapacity
districts.5001.needsRedirection
Enter fullscreen mode Exit fullscreen mode

A fresh run of that demo reported explain().k = 3, with a mutation time of 0.295ms; the later predicate filter over the collection took 60.88ms. That is the core design boundary: mutation propagation stays local, while broad queries still pay for broad selection.

This architecture is not a claim that every operation is constant time. If many derivations subscribe to the same root, then k grows. The Root_Fanout_100k demo exists as the counterexample: one shared source with 100,000 dependents makes the dependency frontier itself large. The optimization is not magic; it is locality.

The same model also works with secret scopes. Secret branches are stored through encryptedBranches, outside the public semantic index. secretEpoch invalidates secret-related caches when the security topology changes, and explain() masks stealth inputs as origin: "stealth" instead of exposing values. In other words, derivation tracking can still operate internally while the public structure remains silent.

A useful implementation pattern emerges:

  1. Store derivations separately from plain values.
  2. Resolve expression inputs into canonical paths at registration time.
  3. Maintain a reverse index from input path to dependent targets.
  4. On mutation, walk the reverse index instead of scanning the graph.
  5. Support eager recomputation for write-time freshness and lazy recomputation for read-time freshness.
  6. Expose recompute waves through debugging tools, but treat observability as a real cost.

That is the optimization implemented in .me: the runtime does not make large graphs cheap by ignoring consistency. It keeps consistency local by making dependencies addressable in reverse.

Source Code
sui Gn
All.This Docs
Inverted Dependency Index

Top comments (0)