Vue.js 84 🟢 Fine-Grained Reactivity Engine Architecture (Alien Signals & Signal Primitives)
Vue’s reactivity system is undergoing its most significant architectural change since Vue 3.0. In Vue 3.6, the core dependency-tracking algorithm is being replaced with Alien Signals, a standalone signal library developed by Vue core contributor Johnson Chu . The change is not a surface-level optimization; it is a rewrite of the mechanism that answers the question “when a value changes, who needs to know?” The previous system, based on Proxy interception and Set-based subscriber collections, is being replaced with a push-pull algorithm built on doubly linked lists, avoiding Array, Set, and Map in its core to minimize memory overhead and garbage collection pressure .
This chapter covers the signal primitives that Alien Signals exposes, the push-pull algorithm that distinguishes it from both pure push and pure pull systems, the doubly linked list structure that replaces Vue’s traditional subscriber sets, and the integration path that brings this algorithm into Vue 3.6 while preserving the existing ref, computed, and watchEffect APIs.
Key point: The performance gain comes from a design constraint, not a complex scheduling strategy. Alien Signals forbids Array, Set, Map, and recursion in its algorithmic core. Under these constraints, maintaining algorithmic simplicity produces larger improvements than sophisticated scheduling would .
Why Alien Signals exists
The pull-based limitation. Vue 3.5 switched to a pull-based reactivity algorithm similar to Preact’s. In a pull model, when a signal changes, computed values are not immediately recalculated. They are marked dirty, and the recalculation happens when the value is read. This avoids unnecessary computation for computed values that are never read, but it introduces overhead at read time: every read must check whether the value is dirty and potentially recompute . For a component that reads many computed values during render, the read-time checks accumulate.
The push-based limitation. A pure push model recalculates every dependent computed value immediately when a signal changes. This guarantees that every computed value is always fresh, but it does unnecessary work when a computed value is not used before the next change. A chain of computed values that is only read at the end of a render cycle would be recalculated multiple times during the cycle, once per intermediate change .
The push-pull hybrid. Alien Signals combines both approaches. When a signal changes, a “dirty” flag is propagated to dependent computed values—this is the push phase. The actual recalculation happens only when the computed value is read—this is the pull phase . The propagation is cheap because it only sets flags; the expensive work is deferred until the value is actually needed. This is the algorithm that has been ported to Vue 3.6 .
The memory problem. Vue’s traditional reactivity system stores subscribers in Set objects and tracks dependencies through Map structures. Every reactive property that has at least one subscriber allocates these objects. In a large application with thousands of reactive properties, the allocation and garbage collection of these structures becomes a measurable cost . Alien Signals replaces the Set with a doubly linked list, where each dependency relationship is a link node with prev and next pointers. Adding and removing subscribers is pointer manipulation rather than hash table operations, and the nodes can be reused .
a. Signal Primitives: signal, computed, effect
Alien Signals exposes three core primitives that map directly to Vue’s ref, computed, and watchEffect. The difference is in the call syntax and the underlying implementation. A signal is a function that returns its current value when called with no arguments and sets a new value when called with an argument. A computed is a function that returns a cached derived value. An effect is a function that re-runs when its dependencies change .
// alien-signals basic API
import { signal, computed, effect } from 'alien-signals'
// signal: read with count(), write with count(newValue)
const count = signal(1)
// computed: derived value, recalculated only when dirty and read
const doubleCount = computed(() => count() * 2)
// effect: runs immediately, re-runs when count changes
effect(() => {
console.log(`Count is: ${count()}`)
})
count(2) // Triggers the effect, prints "Count is: 2"
The mapping to Vue’s APIs is direct but not identical. In Vue, ref(1) returns an object with a .value property. In Alien Signals, signal(1) returns a function. The function-call syntax is deliberate: it avoids the Proxy overhead of Vue’s reactive objects and makes the dependency tracking explicit at the call site. Every call to count() is a tracked read; every call to count(2) is a tracked write .
Vue’s integration preserves the ref API. The underlying signal implementation changes, but the user-facing .value accessor remains. The computed and watchEffect APIs also remain unchanged. The migration is internal, not a breaking change for application code .
b. The Doubly Linked List and Push-Pull Propagation
The traditional Vue reactivity system uses a WeakMap from target objects to Map instances keyed by property name, and each property maps to a Set of subscriber effects . Tracking a dependency adds the current effect to the Set. Triggering a change iterates the Set and runs each effect. The Set provides O(1) insertion and deletion, but it allocates a new Set per reactive property and a new entry per dependency relationship.
Alien Signals replaces the Set with a doubly linked list. Each dependency relationship is a Link node with pointers to the previous and next link in the subscriber chain, and pointers to the dependency and subscriber it connects . When an effect tracks a signal, a link node is created or reused and inserted into the signal’s subscriber list. When the effect is cleaned up or re-run, the link nodes are unlinked rather than deleted, allowing them to be reused .
// Conceptual Link node structure in Alien Signals
interface Link {
dep: Dep // The dependency (signal or computed)
sub: Subscriber // The subscriber (computed or effect)
prevDep: Link | undefined // Previous link for the same subscriber
nextDep: Link | undefined // Next link for the same subscriber
prevSub: Link | undefined // Previous link for the same dependency
nextSub: Link | undefined // Next link for the same dependency
}
The push phase propagates a dirty flag through the dependency graph. When a signal is written, it marks its direct subscribers as dirty and propagates the mark to their subscribers, up to the effect level. The propagation is iterative, not recursive, using an explicit stack to avoid function call overhead and stack depth limits . The pull phase happens when a computed value is read: if it is marked dirty, it recalculates by reading its dependencies, which may themselves be dirty and recalculate on demand .
The effect level is where the pull phase terminates. An effect is always executed when marked dirty; it does not wait to be “read” because effects are not read, they are run. The dirty flag propagation ensures that an effect is marked exactly once per batch of changes, even if multiple dependencies change in the same tick. This is the mechanism that preserves Vue’s batched update behavior while changing the underlying algorithm .
c. Integration with Vue’s Existing Reactivity API
The Alien Signals algorithm is being ported into @vue/reactivity, the package that provides ref, reactive, computed, and effect. The pull request that integrates the algorithm (vuejs/core#12349) replaces the internal dependency tracking structures while preserving the public API . This means that application code using ref and computed continues to work without modification, but the performance characteristics change.
The reported performance improvements vary across sources. The Vue.js Nation presentation cited a 40% performance improvement and a 65% reduction in memory usage . The Alien Signals repository describes the overall performance as approximately 400% that of Vue 3.4’s reactivity system . The discrepancy is likely due to different benchmark scenarios and different baseline versions. The consistent claim is that the algorithm is faster and uses less memory than the system it replaces, particularly in scenarios with many reactive dependencies.
A consequence of the integration is that Vue’s reactivity system becomes more modular. The @vue/reactivity package can be used independently of Vue components, and the signal primitives can be exposed as a standalone API. This aligns with the broader trend toward signals in the frontend ecosystem, where frameworks like Solid, Preact, and Angular have adopted or are adopting signal-based reactivity . Vue’s adoption of Alien Signals positions its reactivity system as compatible with this trend while maintaining the component model that Vue developers expect.
Complete Example Session
// ============================================
// PART 1: SIGNAL PRIMITIVE
// ============================================
import { signal } from 'alien-signals'
// Create a signal with initial value
const count = signal(0)
// Read: call with no arguments
console.log(count()) // 0
// Write: call with a value
count(1)
console.log(count()) // 1
// ============================================
// PART 2: COMPUTED PRIMITIVE
// ============================================
import { computed } from 'alien-signals'
// Derived value, cached until dependencies change
const double = computed(() => count() * 2)
// Reading computed triggers calculation if dirty
console.log(double()) // 2 (count is 1)
count(2)
console.log(double()) // 4 (recalculated on read)
// ============================================
// PART 3: EFFECT PRIMITIVE
// ============================================
import { effect } from 'alien-signals'
// Effect runs immediately and re-runs on dependency change
effect(() => {
console.log(`Count changed to: ${count()}`)
})
// Prints: Count changed to: 2
count(3)
// Prints: Count changed to: 3
// ============================================
// PART 4: PUSH-PULL DIRTY FLAG PROPAGATION
// ============================================
// When count(4) is called:
// 1. Push phase: mark double and effect as dirty
// 2. Pull phase: effect is executed immediately
// 3. double() is recalculated only if read inside effect
// If effect does not read double, double is not recalculated
// ============================================
// PART 5: DOUBLY LINKED LIST SUBSCRIBERS
// ============================================
// Conceptual: signal's subscriber list
// count.subs = Link(effect) <-> Link(double)
// Each link has prevSub/nextSub pointers
// No Set allocation per signal
// ============================================
// PART 6: VUE INTEGRATION — REF PRESERVED
// ============================================
// Vue 3.6 keeps the ref API
// const count = ref(0)
// count.value // still works
// Internally, the signal algorithm replaces the Set-based tracking
// ============================================
// PART 7: COMPUTED CHAIN WITH PUSH-PULL
// ============================================
// const a = signal(1)
// const b = computed(() => a() + 1)
// const c = computed(() => b() * 2)
// When a changes:
// Push: mark b dirty, mark c dirty
// Pull: c() read → b() read → a() read → recalculate c
// ============================================
// PART 8: EFFECT BATCHING
// ============================================
// Multiple signal writes in same tick
// mark effect dirty once, run once
// Preserves Vue's batched update behavior
// ============================================
// PART 9: MEMORY EFFICIENCY
// ============================================
// Traditional: WeakMap -> Map -> Set -> effect
// Alien Signals: Link nodes with 4 pointers
// No hash table allocation per property
// ============================================
// PART 10: STANDALONE USAGE
// ============================================
// @vue/reactivity can be used without Vue components
// Signal primitives are framework-agnostic
// createReactiveSystem() allows custom surface APIs
The ten parts trace the signal primitives, the push-pull propagation, and the integration with Vue’s existing API. The critical distinction is in Part 4: the dirty flag is propagated eagerly (push), but the recalculation is deferred until the value is read (pull).
Quick Reference
Signal Primitives
| Primitive | Read Syntax | Write Syntax | Vue Equivalent |
|---|---|---|---|
signal(value) | signal() | signal(newValue) | ref(value) / .value |
computed(fn) | computed() | — | computed(fn) |
effect(fn) | — | — | watchEffect(fn) |
Algorithm Comparison
| Aspect | Pull (Vue 3.5) | Push (Traditional) | Push-Pull (Alien Signals) |
|---|---|---|---|
| Change propagation | Lazy | Immediate | Dirty flag only |
| Recalculation | On read | On change | On read if dirty |
| Unused computed | Not computed | Computed anyway | Not computed |
| Read overhead | Dirty check | None | Dirty check |
Data Structures
| System | Subscriber Storage | Allocation |
|---|---|---|
| Vue traditional | Set per property | New Set per tracked property |
| Alien Signals | Doubly linked list | Link nodes, reused |
Integration Facts
| Fact | Detail |
|---|---|
| PR | vuejs/core#12349 |
| Target version | Vue 3.6 |
| Reported speed gain | ~40% to ~400% (benchmark-dependent) |
| Reported memory gain | ~65% reduction |
| API change | None; ref/computed preserved |
Best Practices
✅ Do This:
// Use Vue's ref/computed APIs as before
// The implementation changes, not the interface
const count = ref(0) // ✅
// Understand that computed values are lazy
// They recalculate only when read and dirty
const double = computed(() => count.value * 2) // ✅
// Use effect() for side effects that depend on signals
effect(() => { console.log(count()) }) // ✅
❌ Don’t Do This:
// Rely on internal Set/Map structures
// They are replaced by linked lists in Vue 3.6
// Do not access reactivity internals // ❌
// Assume computed values recalculate eagerly
// In push-pull, recalculation is deferred until read // ❌
// Use Alien Signals directly in Vue application code
// Use Vue's ref/computed APIs; the signal package is internal // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Computed value is stale | Read before dirty flag propagation completes | Read within effect after change |
| Effect runs more than expected | Dirty flag set multiple times in batch | Accept; batching prevents multiple runs |
| Signal read outside tracking context | No active effect | Wrap in effect or computed |
| Memory leak from unreleased links | Subscriber not cleaned up | Use effectScope or component lifecycle |
Real-World Examples
1. Create a Signal
const count = signal(0)
2. Read a Signal
count()
3. Write a Signal
count(1)
4. Create Computed
const double = computed(() => count() * 2)
5. Create Effect
effect(() => { console.log(count()) })
6. Dirty Flag Propagation
count changes → mark double dirty → mark effect dirty
7. Pull Recalculation
effect runs → reads double() → double is dirty → recalculates
8. Linked List Subscriber
signal.subs = Link(effect) <-> Link(double)
9. Vue Integration
ref(0) internally uses signal(0) in Vue 3.6
10. Standalone Use
import { signal, computed, effect } from 'alien-signals'
Visual
Push-Pull Propagation
┌──────────────────────────────────────────────────────────────┐
│ SIGNAL CHANGES │
│ │
│ count(4) │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ PUSH PHASE │ Mark dirty (cheap) │
│ │ │ │
│ │ double.dirty = true │
│ │ effect.dirty = true │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ PULL PHASE │ Recalculate on read (expensive) │
│ │ │ │
│ │ effect runs → reads double() │
│ │ double is dirty → recalculates → reads count() │
│ └─────────────────┘ │
│ │
│ Unused computed values are never recalculated. │
│ Only the dirty flag is propagated. │
│ │
└──────────────────────────────────────────────────────────────┘
Linked List vs. Set
┌──────────────────────────────────────────────────────────────┐
│ TRADITIONAL (Set) ALIEN SIGNALS (Linked List) │
│ │
│ signal.subs = Set { signal.subs = Link <-> Link │
│ effect, │ │ │
│ double ▼ ▼ │
│ } effect double │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Hash table │ │ Pointer │ │
│ │ allocation │ │ manipulation │ │
│ │ per property │ │ no allocation │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Link node: │
│ ┌─────────────────────────────────────────┐ │
│ │ dep: Signal │ │
│ │ sub: Effect │ │
│ │ prevSub, nextSub, prevDep, nextDep │ │
│ └─────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
Signal Primitives and Vue Mapping
┌──────────────────────────────────────────────────────────────┐
│ ALIEN SIGNALS VUE │
│ │
│ signal(0) ref(0) │
│ count() count.value │
│ count(1) count.value = 1 │
│ │
│ computed(() => ...) computed(() => ...) │
│ double() double.value │
│ │
│ effect(() => ...) watchEffect(() => ...) │
│ │
│ The API shape differs; the semantics are the same. │
│ Vue preserves the .value interface for compatibility. │
│ │
└──────────────────────────────────────────────────────────────┘
Vue 3.6 Integration
┌──────────────────────────────────────────────────────────────┐
│ VUE 3.6 │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ @vue/reactivity │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ Alien Signals core algorithm │ │ │
│ │ │ (push-pull, linked list) │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ ref, computed, watchEffect │ │ │
│ │ │ (public API unchanged) │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
│ PR: vuejs/core#12349 │
│ Target: Vue 3.6 │
│ │
│ Application code does not change. │
│ Performance characteristics do. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Algorithm | Push-pull hybrid |
| Signal primitive | signal(value) → signal() / signal(v) |
| Computed primitive | computed(fn) → computed() |
| Effect primitive | effect(fn) |
| Push phase | Mark dirty flags |
| Pull phase | Recalculate on read |
| Data structure | Doubly linked list (no Set/Map) |
| Constraint | No Array/Set/Map, no recursion |
| Vue integration | @vue/reactivity in Vue 3.6 |
| PR | vuejs/core#12349 |
Key takeaways:
- Push-pull combines the advantages of both models. The push phase propagates a dirty flag cheaply; the pull phase defers recalculation until the value is read. Unused computed values are never recalculated, and used values are recalculated only once per batch .
- The doubly linked list replaces the
Set. Each dependency relationship is a link node with four pointers. Adding and removing subscribers is pointer manipulation, not hash table operations. The nodes can be reused, reducing allocation and garbage collection pressure . - The design constraints are the performance strategy. Alien Signals forbids
Array,Set,Map, and recursion in its core. Under these constraints, algorithmic simplicity produces larger gains than complex scheduling would. The reported performance improvement over Vue 3.4 is substantial, though the exact number depends on the benchmark . - Vue’s public API does not change. The
ref,computed, andwatchEffectAPIs remain. The.valueaccessor forrefis preserved. The signal primitives are an internal implementation detail, not a new API surface for Vue application developers . - The integration positions Vue within the broader signals trend. Solid, Preact, and Angular have adopted or are adopting signal-based reactivity. Vue’s adoption of Alien Signals makes its reactivity system compatible with this direction while maintaining the component model and template syntax that distinguish Vue .
Remember: Alien Signals is not a new feature for Vue developers to learn. It is a replacement for the engine underneath the features they already use. The ref you write in a component still has a .value. The computed you define still caches its result. The watchEffect you register still re-runs when its dependencies change. What changes is how the engine knows which dependencies to track and when to recalculate. The push-pull algorithm and the linked list are the mechanism. The performance improvement is the outcome. The API stability is the constraint that makes the migration invisible to application code.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!