This project explores a push-pull based signal algorithm. The implementation is related to the following frontend projects:
- Propagation algorithm of Vue 3
- Preact’s double-linked-list approach (https://preactjs.com/blog/signal-boosting/)
- Inner effects scheduling of Svelte
- Graph-coloring approach of Reactively (https://milomg.dev/2022-12-01/reactivity)
We impose some constraints (such as not using Array/Set/Map and disallowing function recursion in the algorithmic core) to ensure performance. We found that under these conditions, maintaining algorithmic simplicity offers more significant improvements than complex scheduling strategies.
I wrote the reactivity code for both Vue and alien-signals. Below is a benchmark comparison against Vue 3.4 and other frameworks. The core algorithm has since been ported back to Vue 3.6.
ImageBenchmark repo: https://github.com/transitive-bullshit/js-reactivity-benchmark
I spent considerable time optimizing Vue 3.4’s reactivity system, gaining experience along the way. Since Vue 3.5 switched to a pull-based algorithm similar to Preact, I decided to continue researching a push-pull based implementation in a separate project. The algorithm is used in Vue language tools for incremental AST parsing and virtual code generation.
- Dart: medz/alien-signals-dart
- Dart: void-signals/void_signals
- Lua: YanqingXu/alien-signals-in-lua
- Lua 5.4: xuhuanzy/alien-signals-lua
- Luau: Nicell/alien-signals-luau
- Java: CTRL-Neo-Studios/java-alien-signals
- C#: CTRL-Neo-Studios/csharp-alien-signals
- Go: delaneyj/alien-signals-go
- Rust: wuzekang/samara-signals
- Rust: ohkami-rs/alien-signals-rs
- Rajaniraiyn/react-alien-signals: React bindings for the alien-signals API
- CCherry07/alien-deepsignals: Use alien-signals with the interface of a plain JavaScript object
- hunghg255/reactjs-signal: Share Store State with Signal Pattern
- gn8-ai/universe-alien-signals: Enables simple use of the Alien Signals state management system in modern frontend frameworks
- WebReflection/alien-signals: Preact signals like API and a class based approach for easy brand check
- @lift-html/alien: Integrating alien-signals into lift-html
- ilha: A tiny web UI library built around the islands architecture
- @sigrea/core: Signals, deep reactivity, and molecule lifecycles built on alien-signals
- @lazy-promise/alien-signals: Async signals built on top of alien-signals and LazyPromise
- vuejs/core: The core algorithm has been ported to v3.6 (PR: vuejs/core#12349)
- statelyai/xstate: The core algorithm has been ported to implement the atom architecture (PR: statelyai/xstate#5250)
- flamrdevs/xignal: Infrastructure for the reactive system
- vuejs/language-tools: Used in the language-core package for virtual code generation
- unuse: A framework-agnostic
uselibrary inspired byVueUse
import { signal, computed, effect } from 'alien-signals'; const count = signal(1); const doubleCount = computed(() => count() * 2); effect(() => { console.log(`Count is: ${count()}`); }); // Console: Count is: 1 console.log(doubleCount()); // 2 count(2); // Console: Count is: 2 console.log(doubleCount()); // 4
import { signal, effect, effectScope } from 'alien-signals'; const count = signal(1); const stopScope = effectScope(() => { effect(() => { console.log(`Count in scope: ${count()}`); }); // Console: Count in scope: 1 }); count(2); // Console: Count in scope: 2 stopScope(); count(3); // No console output
Effects can be nested inside other effects. When the outer effect re-runs, inner effects from the previous run are automatically cleaned up, and new inner effects are created if needed. The system ensures proper execution order — outer effects always run before their inner effects:
import { signal, effect } from 'alien-signals'; const show = signal(true); const count = signal(1); effect(() => { if (show()) { // This inner effect is created when show() is true effect(() => { console.log(`Count is: ${count()}`); }); } }); // Console: Count is: 1 count(2); // Console: Count is: 2 // When show becomes false, the inner effect is cleaned up show(false); // No output count(3); // No output (inner effect no longer exists)
The trigger() function allows you to manually trigger updates for downstream dependencies when you've directly mutated a signal's value without using the signal setter:
import { signal, computed, trigger } from 'alien-signals'; const arr = signal<number[]>([]); const length = computed(() => arr().length); console.log(length()); // 0 // Direct mutation doesn't automatically trigger updates arr().push(1); console.log(length()); // Still 0 // Manually trigger updates trigger(arr); console.log(length()); // 1
You can also trigger multiple signals at once:
import { signal, computed, trigger } from 'alien-signals'; const src1 = signal<number[]>([]); const src2 = signal<number[]>([]); const total = computed(() => src1().length + src2().length); src1().push(1); src2().push(2); trigger(() => { src1(); src2(); }); console.log(total()); // 2
You can reuse alien-signals’ core algorithm via createReactiveSystem() to build your own signal API. For implementation examples, see:
- Starter template (implements
.get()&.set()methods like the Signals proposal) - stackblitz/alien-signals/src/index.ts
- proposal-signals/signal-polyfill#44
The actual implementations of propagate and checkDirty in system.ts replace recursive calls with iterative stack-based traversal for performance. The recursive versions below are equivalent and easier to follow — useful as a reference when porting to other languages where the iterative optimization may not help.
propagate
function propagate(link: Link, innerWrite: boolean): void { do { const sub = link.sub; let flags = sub.flags; if (!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending))) { sub.flags = flags | ReactiveFlags.Pending; if (innerWrite) { sub.flags |= ReactiveFlags.Recursed; } } else if (!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))) { flags = ReactiveFlags.None; } else if (!(flags & ReactiveFlags.RecursedCheck)) { sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending; } else if (!(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) && isValidLink(link, sub)) { sub.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending; flags &= ReactiveFlags.Mutable; } else { flags = ReactiveFlags.None; } if (flags & ReactiveFlags.Watching) { notify(sub); } if (flags & ReactiveFlags.Mutable) { const subSubs = sub.subs; if (subSubs !== undefined) { propagate(subSubs, innerWrite); } } link = link.nextSub!; } while (link !== undefined); }
checkDirty
function checkDirty(link: Link, sub: ReactiveNode): boolean { do { const dep = link.dep; const depFlags = dep.flags; if (sub.flags & ReactiveFlags.Dirty) { return true; } else if ((depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) === (ReactiveFlags.Mutable | ReactiveFlags.Dirty)) { if (update(dep)) { const subs = dep.subs!; if (subs.nextSub !== undefined) { shallowPropagate(subs); } return true; } } else if ((depFlags & (ReactiveFlags.Mutable | ReactiveFlags.Pending)) === (ReactiveFlags.Mutable | ReactiveFlags.Pending)) { if (checkDirty(dep.deps!, dep)) { if (update(dep)) { const subs = dep.subs!; if (subs.nextSub !== undefined) { shallowPropagate(subs); } return true; } } else { dep.flags = depFlags & ~ReactiveFlags.Pending; } } link = link.nextDep!; } while (link !== undefined); return false; }