Interactive Demos
See the Diamond Dependency Glitch in Action
Section titled “See the Diamond Dependency Glitch in Action”When two derived values depend on the same source (a diamond dependency), some reactive libraries emit inconsistent intermediate states — known as glitches. This demo lets you see the difference with your own eyes.
How it works
Section titled “How it works”Click and drag on each canvas to draw a trail. Each dot is placed by the library’s reactive pipeline using the same diamond dependency graph:
mousePos (source: {x, y}) ├── derivedX = map(pos => pos.x) ├── derivedY = map(pos => pos.y) └── combine([derivedX, derivedY]) └── draw dot at {x, y} + check consistencyEach emitted position is compared to the latest mouse position:
- Blue dot — emitted
(x, y)matches the actual mouse position (consistent). - Red dot — one axis updated but the other didn’t yet (glitch — the dot appears off the drag path).
What to look for
Section titled “What to look for”| Library | Glitches | Why |
|---|---|---|
| SynState | 0 | derivedX and derivedY are both recomputed, in depth order, before combine and before any subscriber |
| RxJS | Many | combineLatest emits as soon as derivedX changes, while derivedY still holds the previous position, then again |
| Jotai | 0 | The subscribed atom is recomputed when it is read, after both inputs |
| MobX | 0 | The reaction reads the computed values after the action that moved the mouse, when every input is up to date |
| Preact Signals | 0 | The effect reads computed values, and each is recomputed on read once one of its sources has a newer version |
| Solid | 0 | Every memo below the write is marked first, and the memos rerun before the effect, which then sees both new values |
Five of the six are glitch-free on this diamond, since in each of the five the guarantee covers derived values. Where they part is a step written as an effect or reaction that writes a value, such as one that starts a request; Operators on Signals shows that case. RxJS’s extra emissions also grow with the graph, which the next demo counts.
Why glitches matter
Section titled “Why glitches matter”In real applications, diamond dependencies appear everywhere — computed styles from shared state, derived UI values, form validations. Glitches cause:
- Visual flicker — UI renders an impossible intermediate state.
- Wasted work — each extra emission triggers unnecessary side effects. With inputs to
combineLatest, a single source update causes emissions — this scales to an performance cliff as shown in the demo below. - Logic bugs — side effects fire with inconsistent data.
A glitch-free library, SynState among them, rules these out at the library level, with no manual workarounds needed.
Deep Dependency Chain: Propagation Under Load
Section titled “Deep Dependency Chain: Propagation Under Load”This demo builds the same reactive graph in each of six libraries: a depth- chain of stateful nodes where each depends on the previous, with a final combine/derive that reads all outputs.
A snake tail follows the mouse — each segment is a stage in the chain that smoothly follows the previous stage via linear interpolation (lerp). To draw the full snake, all outputs (head + stages) are collected into a single combine:
The diagram shows . In the demo, is adjustable via a slider. Increase the chain depth and compare the Total updates counters.
What to look for
Section titled “What to look for”- Total updates counts the calls to each library’s subscriber. Five of the six count one per mouse move, whatever is.
- RxJS counts roughly per mouse move, so its counter pulls away as grows, and at high its canvas stutters.
- Avg μs/update is how long each subscriber call takes to draw the snake. It leaves out the propagation, so it is about the same for every library; the difference is in how many calls there are. The benchmark’s deep chain scenario measures propagation through a chain of this shape.
Why RxJS collapses at high N
Section titled “Why RxJS collapses at high N”Check the Total updates counter: RxJS shows roughly times more updates than SynState for the same mouse movement. combineLatest fires every time any of its inputs emits. On a single mouse move, the scan chain propagates sequentially:
sourceemits →combineLatestfires (only source updated, scans still hold old values)scan₁emits →combineLatestfires againscan₂emits → fires again- … for all stages
Each of these firings triggers a full canvas redraw of points → . This is the glitch problem from the first demo manifesting as a performance cliff.
Summary: all six libraries compared
Section titled “Summary: all six libraries compared”Each library writes a stateful stage in its own idiom, shown in the second column; Evaluation Models describes how each one propagates in general.
| Library | Stages in this demo | Subscriber calls per mouse event | Work per event |
|---|---|---|---|
| SynState | scan operators, pushed in depth order from the root | 1 (combine) | |
| RxJS | scan operators, pushed depth-first per subscription | (combineLatest) | |
| Jotai | selectAtom atoms, each derived from the one before | 1 (store.sub) | |
| MobX | Boxes that one reaction writes in a single action | 1 (autorun) | |
| Preact Signals | Signals that one effect writes in a single batch | 1 (effect) | |
| Solid | Memos given their previous value, rerun after marking | 1 (createEffect) |
Every library but RxJS calls its subscriber once per mouse event, so each does work proportional to . What one update costs within that is a constant that depends on the library and on the shape of the graph; the next demo makes it visible, and the benchmark measures it.
Throughput: Per-Update Overhead Under Load
Section titled “Throughput: Per-Update Overhead Under Load”This demo leaves out RxJS, whose update count would dominate, and runs the five libraries that call their subscriber once per update.
How it works
Section titled “How it works”A ball automatically orbits in a circle. Each canvas runs the same depth- reactive chain as the spring demo above. The difference: instead of 1 source update per mouse event, source updates are pushed through the chain every animation frame in a tight synchronous loop.
each animation frame: for k in 0..K-1: update source position (micro-step along orbit) → propagate through depth-M scan chain → subscriber records latest points draw snake onceThis amplifies the per-update overhead by a factor of . When the overhead exceeds 16ms, the animation drops frames and stutters.
What to look for
Section titled “What to look for”- ms/frame is how long the updates of one frame take, measured in this browser on this device. It turns red past 16ms, the budget of a 60fps frame.
- Raise and until a canvas stutters. Which one does first, and at what , depends on the browser, the device and the rest of the page, so these numbers are not a benchmark: the benchmark’s deep chain scenario runs this same graph under controlled conditions.
- What each library does per update, and when, is in Evaluation Models.
What’s Next?
Section titled “What’s Next?”- Performance Benchmark — quantitative measurements of the same scenarios shown here, plus additional graph topologies.
- How SynState Solved the Glitch — deep dive into the depth-ordered propagation algorithm.
- Library Comparison — feature-by-feature comparison of SynState with RxJS, Jotai, MobX, and others.