Created With


To obtain a proper analysis of RxDeep's performance (and generally complexity of the problem it tries to solve), we need to establish a few definitions first. Assume we have a state tree looking like this:

1link{ // --> root of the state-tree

2link name: 'Cool Team', // --> a leaf node

3link people: [ // --> a middle node

4link { // --> a middle node

5link name: 'Jerome', // --> a leaf node

6link age: 42, // --> a leaf node

7link address: { // --> a middle node

8link city: 'Jersey', // --> a leaf node

9link country: 'Jibuti' // --> a leaf node

10link },

11link },

12link { // --> a middle node

13link name: 'Jacqueline', // --> a leaf node

14link age: 27, // --> a leaf node

15link address: { // --> a middle node

16link city: 'Jakarta', // --> a leaf node

17link country: 'Jamaica' // --> a leaf node

18link }

19link }

20link ]


Two parameters of this tree are particularly important to analyzing complexity of the reactive change propagation problem:

We will express complexity and performance of algorithms using the Big O notation, which is displayed with O(f(n,d))\Omicron(f(n, d)) syntax, since the complexity is always expressed as a function of nn and dd.

Parameters nn and dd are independent. State-tree can be an array with a thousand strings, where we would have nn = 1000, dd = 1, or it can be a single string nested in 1000 arrays ([[[...['x']...]]]), where we would have nn = 1, dd = 1000. We say a state-tree is typical, if dd is in the order of log(n)\log(n), i.e.

O(d)=O(log(n)).\Omicron(d) = \Omicron(\log(n)).1

In practice, most of the time you have lower bounds and upper bounds on average number of childs each node of the state-tree has, which would mean that in practice most state-trees are typical.

linkChange in Leaf Nodes

For changes in a single leaf node, the node itself should emit a change, its parent should emit a change, the parent of its parent should emit a change, and all the path from the node to the root state should emit changes:

1linkconst root = state([{address: { city: 'Jakarta' }}]);

2linkconst mid1 = root.sub(0);

3linkconst mid2 = mid1.sub('address');

4linkconst leaf = mid2.sub('city');


6linkleaf.value = 'Jacksonville';


8link// Now leaf, mid2, mid1 and root must emit changes.

This means that each change in a leaf node must result in at least O(d)\Omicron(d) operations, independent of the underlying algorithm. For a typical state-tree, that would be O(log(n))\Omicron(\log(n)) operations.

For each such change, RxDeep performs one assignment by reference (to update local state's value) without emission, and constructs a trace object, passing the change with the trace object to the upstream. This would result in O(d)\Omicron(d) operations until the change reaches the root of the state-tree (alongside O(d)\Omicron(d) trace objects).

The root state then typically bounces the change back. Each recepient state will emit the new value, and perform equality checks between all of its generated sub-keys and head of received trace. On match, it will extract the downstream trace and pass down the resulting change to the matching sub-key. Assuming an average number of kk sub-states per state, this yields O(kd)\Omicron(kd) equality checks and O(d)\Omicron(d) emissions until the change is fully propagated across the state-tree.

Since kk is a constant, this overall yields O(d)\Omicron(d) operations to fully propagate each change across the state-tree, O(log(n))\Omicron(\log(n)) for typical state-trees, matching the lower complexity bound for any solution.

linkChange in Arbitrary Nodes

Change in an arbitrary node might result in a change in all of the nodes of a state-tree, which subsequently should result in O(nd)\Omicron(nd) change emissions (O(nlog(n))\Omicron(n \log(n)) for a typical state-tree).

More specifically, if a change is issued at a node at depth δ\delta, with a sub-tree of nδn_{\delta} nodes, then nodes on the path from the changed node up to the root should emit a change (O(δ)\Omicron(\delta) emissions), and possibly the whole sub-tree under the changed node should also emit (O((dδ)nδ)\Omicron((d - \delta)n_{\delta}) emissions). Overall this would amount to O(δ+(dδ)nδ)\Omicron(\delta + (d - \delta)n_{\delta}) operations. Assuming a typical tree, we would have:

O(log(nδ))=O(log(n)δ)\Omicron(\log(n_{\delta})) = \Omicron(\log(n) - \delta)1

Which would give the minimum number of operations for change at depth δ\delta at:

O(log(n)+(nδ1)log(nδ))=O(δ+(dδ)2dδ)\Omicron(\log(n) + (n_{\delta} - 1)\log(n_{\delta})) = \Omicron(\delta + (d - \delta)2^{d - \delta})1

Maximized on δ=0\delta = 0 or nδ=nn_{\delta} = n, i.e. on the root state, and minimized on δ=d\delta = d or nδ=1n_{\delta} = 1, i.e. a leaf state.

1linkconst root = state([{address: { city: 'Jakarta', country: 'Indonesia' }}]);

2linkconst mid1 = root.sub(0);

3linkconst mid2 = mid1.sub('address');

4linkconst leaf1 = mid2.sub('city');

5linkconst leaf2 = mid2.sub('country');


7linkmid2.value = { city: 'Jambi', country: 'Indonesia' };


9link// due to this change, mid2 itself, mid1, root and leaf1 should re-emit.

10link// however the value for leaf2 hasn't changed and it shall not re-emit.

From the changed state upwards (towards the root), RxDeep behaves as before as it can fully track changes, resulting in O(δ)\Omicron(\delta) or O(log(n)log(nδ))\Omicron(\log(n) - \log(n_{\delta})) operations (the latter holding for a typical state-tree).

For downward propagation, the state conducts a sweep of its sub-tree and complete the trace. This is called post-tracing, and in worst-case scenario it requires O(nδlog(nδ))\Omicron(n_{\delta}\log(n_{\delta})) operations. After that changes can be down-propagated precisely, and since in worst-case all of the sub-tree nodes would need to emit due to changes, adding another O(nδlog(nδ))\Omicron(n_{\delta}\log(n_{\delta})) emissions.

This means in total the change propagation mechanism requires O(log(n)+(2nδ1)log(nδ))\Omicron(\log(n) + (2n_{\delta} - 1)\log(n_{\delta})) operations, which is of the same order at the minimum calculated above, i.e.

O(log(n)+nδlog(nδ))\Omicron(\log(n) + n_{\delta}\log(n_{\delta}))1

linkKeyed States

A Keyed State will track changes based on mapping of objects of it content array to particular keys using a given key function instead of using indexes. While doing so, it also produces detailed ListChange objects outlining exactly how the array has changed in terms of additions, deletions and items being moved from a particular index to another.

For doing so, KeyedState conducts O(n)\Omicron(n) operations, also maintaining a key map of O(n)\Omicron(n) size holding quick-access references by keys to elements producing those keys for the previous value. Here, nn denotes not the number of leafs of the whole state-tree but the number of elements of the array assigned to a particular KeyedState.

For each change to the array, all elements are mapped once to their corresponding keys using the provided key function. The key function is supposed to be extremely light in nature, fetching a particular parameter of each element or doing a simple and fast computation:

1link// a key function should be something in lines of these:

2linkp => p.id;

3linkn => n % 17;

If the key function does not have complexity of O(1)\Omicron(1), say O(λ)\Omicron(\lambda) for some non-constant λ\lambda, then upon each change the KeyedState would be conducting O(λn)\Omicron(\lambda n) operations.

When indexed items in the array are moved around, the KeyedState also conducts partial retracing to generate a change trace invariant to its keys. This does not involve invoking the key function, but worst-case can add O(nlog(n))\Omicron(n\log(n)) operations to change propagation process.

This worst case only occurs when all of the items of the array are shifted and their corresponding values are also mutated all the way to the leafs. In a typical case when the items of the array are moved without being modified themselves, then this lowers to O(n)\Omicron(n) operations.

PerformanceChange in Leaf NodesChange in Arbitrary NodesKeyed States

Home How to Install State Key Tracking Change Verification Change Performance Precision