Something unusual is happening in frontend JavaScript: competing frameworks are converging on the same core idea. Angular fully integrated signals in v17 and made them the recommended reactive primitive going forward. Svelte rewrote its reactivity model around "Runes" (its name for signals) in version 5. SolidJS has been signals-native since inception and influenced every other framework's evolution. Vue's Composition API ref() and reactive() are signals in everything but name. Even React is being discussed—at the TC39 level—in the context of a Signals proposal that could land in JavaScript itself.
This is not coincidence or trend-chasing. It is convergence on a model that is demonstrably better for a specific class of problem: managing UI state in large, interactive applications without unnecessary re-rendering.
What Signals Actually Are
A signal is a reactive value container: a cell that holds a value and automatically notifies anything that reads it when the value changes. That notification is synchronous, direct, and fine-grained—only the exact computations and UI nodes that depend on a signal re-run when it changes.
This sounds simple. The implications are profound.
// A minimal signal implementation to illustrate the concept
class Signal<T> {
private _value: T;
private subscribers = new Set<()=> void>();
constructor(initialValue: T) {
this._value = initialValue;
}
get value(): T {
// Track who is reading this signal (dependency tracking)
if (currentEffect) {
this.subscribers.add(currentEffect);
}
return this._value;
}
set value(newValue: T) {
this._value = newValue;
// Notify only the subscribers that depend on this specific signal
this.subscribers.forEach(subscriber => subscriber());
}
}
// Usage
const count = new Signal(0);
const doubled = new Computed(() => count.value * 2); // Derived signal
// Only this specific DOM update runs when count changes
effect(() => {
document.getElementById('counter').textContent = String(count.value);
});
count.value = 5; // Triggers only the effect above, nothing else
Compare this to how React's virtual DOM works: when setState is called, React re-runs the entire component function, computes a new virtual DOM tree, diffs it against the previous tree, and applies the minimal set of DOM updates. Every ancestor and sibling component has the opportunity to re-render (unless explicitly memoized with memo, useMemo, or useCallback).
Signals invert this. Instead of "re-run everything and figure out what changed," signals say "run only what is directly subscribed to the specific value that changed." The computational difference is significant in applications with large component trees and frequent state updates.
The Framework Comparison
Angular Signals (v17+)
Angular's signal integration is the most architecturally significant because Angular previously used a completely different model: Zone.js, which monkey-patched async APIs to detect when anything in the application might have changed, then ran change detection across the entire component tree. This worked but was notoriously difficult to optimize.
Angular signals replace Zone.js patching with explicit, fine-grained dependency tracking:
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-cart',
template: `
<div>
<p>Items: {{ itemCount() }}</p>
<p>Total: {{ total() | currency }}</p>
<button (click="addItem()">Add Item</button>
</div>
`,
})
export class CartComponent {
items = signal<CartItem[]>([]);
// Computed signals: only recalculate when `items` changes
itemCount = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price * item.quantity, 0)
);
constructor() {
// Effects run synchronously when dependencies change
effect(() => {
console.log(`Cart updated: ${this.itemCount()} items`);
// Automatic dependency tracking — no explicit dependency array needed
});
}
addItem() {
this.items.update(current => [
...current,
{ id: crypto.randomUUID(), name: 'New Item', price: 9.99, quantity: 1 },
]);
}
}
The Angular team reports 40–60% improvements in change detection performance for large applications migrated from Zone.js to signals. The developer experience improvement is arguably larger: no more ChangeDetectionStrategy.OnPush, no more manual markForCheck() calls, no more trackBy functions to prevent list re-rendering.
Svelte 5 Runes
Svelte 5's "Runes" system is the most syntactically radical implementation. Rather than exposing a signal() API that returns an object with .value, Svelte runes use compiler directives ($state, $derived, $effect) that the Svelte compiler transforms into optimized signal-based code:
<script>
// $state is a rune — the compiler handles the signal plumbing
let count = $state(0);
let name = $state('World');
// $derived automatically tracks its dependencies
let message = $derived(`Hello, ${name}! Count is ${count}.`);
let doubled = $derived(count * 2);
// $effect runs when any dependency changes
$effect(() => {
document.title = `Count: ${count}`;
// No cleanup return needed for simple effects — Svelte handles it
});
function increment() {
count++; // Direct mutation — Svelte's compiler wraps this in a setter
}
</script>
<h1>{message}</h1>
<p>Doubled: {doubled}</p>
<button onclick={increment}>Increment ({count})</button>The count++ line looks like a mutation, but the Svelte compiler transforms it into a signal setter call. This allows developers to write familiar imperative code while the underlying runtime uses fine-grained reactivity. The tradeoff: Svelte's magic is opaque, and debugging compiled output can be disorienting.
SolidJS: The Original Reference Implementation
SolidJS has used signals since its founding and is widely credited with demonstrating that signals could power a production-grade framework. Solid's JSX compiles to direct DOM operations—no virtual DOM, no diffing, no component re-renders:
import { createSignal, createMemo, createEffect } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2); // Computed signal
createEffect(() => {
// Runs synchronously when count() changes
console.log('Count changed to:', count());
});
return (
<div>
{/* Only this specific text node updates — not the entire component */}
<p>Count: {count()}</p>
<p>Doubled: {doubled()}</p>
<button onClick={()=> setCount(c=> c + 1)}>Increment</button>
</div>
);
}Solid's performance benchmarks consistently place it at the top of frontend framework comparisons—often competitive with vanilla JavaScript. The cost: a smaller ecosystem than React or Angular, a smaller hiring pool, and JSX semantics that differ subtly from React's (components in Solid run once, not on every state change).
Vue Composition API
Vue's ref() and reactive() from the Composition API are signals under a different name. Vue 3's reactivity system uses Proxy-based tracking that is architecturally identical to signals in its dependency-tracking behavior:
import { ref, computed, watchEffect } from 'vue';
const count = ref(0); // Signal
const doubled = computed(() => count.value * 2); // Derived signal
watchEffect(() => {
// Runs when any reactive dependency changes
console.log(`Count: ${count.value}`);
});
count.value++; // Triggers only dependent effects
Vue's Composition API was introduced in Vue 3 and has become the recommended approach over the Options API. For teams already on Vue, this means signals-based reactivity is already available without a migration.
Why This Matters for React
React does not have signals—yet. React's model is fundamentally different: components re-render, and the framework optimizes which components actually need to produce DOM changes. React 19's compiler (formerly "React Forget") automatically inserts memoization to avoid unnecessary re-renders, which is an attempt to get signal-like performance without changing the programming model.
The TC39 Signals proposal is a different approach: standardize signals at the JavaScript language level so frameworks can interoperate. If this lands, React could adopt signals without forcing an ecosystem-breaking change.
For today's React developers, the practical implications:
- React 19 compiler: Enables fine-grained optimization without explicit
useMemo/useCallback. Adopt it—it is the free performance upgrade. - Signals libraries for React:
@preact/signals-reactandjotaiprovide signal-like primitives that work within React's model. These are viable for performance-critical components. - Long-term: The competitive pressure from Svelte, Solid, and Angular's measured improvements is likely to push React toward first-class signals support in a future major version.
Performance: When Signals Actually Matter
Signals do not universally outperform React's virtual DOM for all application types. The difference is most pronounced in specific scenarios:
High-frequency state updates
// Without signals: every 60fps animation tick causes a full diff
function AnimatedCounter() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e: MouseEvent) => {
setPos({ x: e.clientX, y: e.clientY });
};
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return <div style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }}>●</div>;
}In a signals framework, this updates only the specific style attribute of the specific DOM node—not the component, not the virtual DOM, not any parent. For animation and real-time UI, this translates to measurably smoother frame rates.
Large component trees with localized state
In a 500-node component tree where state at the leaf level changes frequently, React must traverse the tree to determine what needs updating even with memoization. Signals update exactly the nodes subscribed to the changed signal, with O(1) traversal cost.
Where the difference is small or negligible
- Server-rendered content with minimal interactivity
- Small applications where the overhead of virtual DOM diffing is imperceptible
- Form-heavy UIs where state changes are driven by infrequent user input
Practical Migration Path
If your team is on Angular: adopt signals today. The Angular team has published an official migration guide, and the @angular/core APIs are stable. Zone.js support continues, so migration can be incremental—convert one component at a time.
If your team is starting a new project with Svelte: Svelte 5 with Runes is the current stable release. Do not start a new project on Svelte 4.
If your team is on React: adopt the React Compiler if on React 19+. Consider jotai for state that updates frequently. Watch the TC39 Signals proposal—but do not migrate off React preemptively.
If your team is evaluating frameworks for a new greenfield project with performance requirements: SolidJS and Svelte 5 offer the best raw performance. Angular with signals is the best choice if you need enterprise tooling, testing infrastructure, and a large hiring pool with signals experience by 2026.
The Signal Ecosystem in 2026
One underappreciated advantage of signals reaching framework convergence: cross-framework libraries can now be signals-native. Libraries like TanStack Query, which previously had to maintain separate adapters for each framework's reactivity model, can now expose a signals-based API that works consistently across Angular, Svelte, Solid, and Vue.
The TC39 Signals proposal, if standardized, would take this further: framework-agnostic reactive primitives that can be passed between components regardless of the framework they were created in. This is the endgame the frontend community has been working toward for years.
Conclusion
The convergence of major frameworks on signals-based reactivity is not fashion—it is a recognition that the virtual DOM model, while powerful, has a fundamental inefficiency: it describes what to render rather than what changed. Signals describe exactly what changed and update exactly what depends on it.
For Angular and Svelte teams, the migration path is clear and officially supported. For React teams, the React Compiler provides incremental improvement without model changes. For anyone evaluating new technologies in 2026, understanding signals is table stakes—because whatever framework or runtime you are using, you will be working with this model for the next decade.