<!-- Canonical: https://lucioduran.com/blog/signals-reactivity-angular-solid-preact
Language: en
Content-SHA256: 3cf742baa1f37a212c73d715ccd325132fd7f78831e3e20e2ba6a4e0e7261d7c
-->

Frontend

# Signals and Fine-Grained Reactivity: Angular, SolidJS, Preact, and the Shift from Virtual DOM Diffing

2026-02-019 min read

Lucio Durán
Engineering Manager & AI Solutions Architect

Also available in: [Español](<https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact>)

## The Cost of Virtual DOM Diffing

React popularized the virtual DOM in 2013 with an elegant pitch: declare your UI as a function of state, and we'll figure out the minimal DOM mutations. Beautiful in theory. In practice, "figuring out the minimal DOM mutations" means:

1. Re-executing your entire component function on every state change
2. Producing a new virtual DOM tree
3. Running a recursive O(n) diff against the previous tree
4. Batching the resulting DOM patches

The critical issue is step 1. When a piece of state changes in a parent component, React re-executes *every child component* in that subtree by default. The diff algorithm is fast, sure, but you're diffing trees that are 95% identical because only one leaf changed.

```javascript
// This component re-renders when ANY part of appState changes
function Dashboard({ appState }) {
 return (
 <div>
 <Header user={appState.user} />
 <Sidebar notifications={appState.notifications} />
 <MainContent data={appState.data} />
 {/* If appState.notifications changes, MainContent
 still re-executes. The diff catches that its output
 hasn't changed, but the function already ran. */}
 </div>
 );
}
```

React's answer has been ` React.memo `, ` useMemo `, ` useCallback `, manual memoization hints that developers consistently get wrong. I've reviewed production codebases where ` useMemo ` was used on primitive values (pointless) and omitted on expensive computations (defeating the purpose).

## Enter Signals: Surgical Reactivity

Signals flip the model. Instead of top-down re-rendering with bottom-up diffing, signals track dependencies at read-time and update only the specific DOM nodes that depend on changed values.

Here is what [SolidJS](<https://www.solidjs.com/>) does internally when you write:

```javascript
import { createSignal, createEffect } from "solid-js";

const [count, setCount] = createSignal(0);
const doubled = () => count() * 2;

createEffect(() => {
 // Solid tracks that this effect reads `count`
 // When count changes, ONLY this effect re-runs
 console.log("Count is:", count());
});

// In a component:
function Counter() {
 const [count, setCount] = createSignal(0);

 // This JSX compiles to direct DOM operations.
 // The {count()} expression becomes a fine-grained
 // subscription that updates ONE text node.
 return (
 <div>
 <span>{count()}</span>
 <button onClick={() => setCount(c => c + 1)}>+</button>
 </div>
 );
}
```

The compilation output looks roughly like this (simplified):

```javascript
function Counter() {
 const [count, setCount] = createSignal(0);

 const _el = document.createElement("div");
 const _span = document.createElement("span");
 const _text = document.createTextNode("");
 const _btn = document.createElement("button");

 // Direct subscription: signal -> text node
 createRenderEffect(() => {
 _text.data = count();
 });

 _btn.addEventListener("click", () => setCount(c => c + 1));
 _span.appendChild(_text);
 _el.appendChild(_span);
 _el.appendChild(_btn);
 return _el;
}
```

No virtual DOM. No diffing. No component re-execution. The signal writes directly to the text node. I profiled this on a real dashboard I maintain, a monitoring panel with ~400 updating values, and the difference was staggering. React with proper memoization: ~12ms per update cycle. SolidJS: ~0.8ms. That's not a benchmark game; that's a production app rendering real WebSocket data.

## Angular Signals: Incremental Adoption

[Angular](<https://angular.dev/>)'s approach is interesting because they couldn't just blow up their existing model. They had to make signals opt-in and interoperable with the existing zone.js-based change detection.

```typescript
import { Component, signal, computed, effect } from '@angular/core';

@Component({
 selector: 'app-inventory',
 template: `
 <div>
 <p>Items: {{ items().length }}</p>
 <p>Total value: {{ totalValue() }}</p>
 <ul>
 @for (item of filteredItems(); track item.id) {
 <li>{{ item.name }} - {{ item.price | currency }}</li>
 }
 </ul>
 </div>
 `
})
export class InventoryComponent {
 items = signal<Item[]>([]);
 filter = signal('');

 filteredItems = computed(() => {
 const f = this.filter().toLowerCase();
 return this.items().filter(i =>
 i.name.toLowerCase().includes(f)
 );
 });

 totalValue = computed(() =>
 this.filteredItems().reduce((sum, i) => sum + i.price, 0)
 );

 constructor() {
 effect(() => {
 // Automatic dependency tracking, runs when
 // filteredItems() changes, not when unrelated
 // signals change
 console.log(`Showing ${this.filteredItems().length} items`);
 });
 }
}
```

What I find compelling about Angular's implementation is the ` computed() ` primitive. It's lazy, it doesn't recalculate until read, and it caches its value. If ` filter ` changes but produces the same ` filteredItems ` result, ` totalValue ` won't recompute. That glitch-free guarantee matters in complex dependency graphs.

The internal implementation uses a version counter on each signal node. When you call ` signal.set() `, it increments the version. Computed signals check upstream versions before recalculating, if nothing changed, they return the cached value without traversing the dependency graph.

## Preact Signals: The Trojan Horse

[Preact](<https://preactjs.com/>) Signals did something audacious: they made signals work inside React. The ` @preact/signals-react ` package intercepts component rendering and replaces signal reads with fine-grained subscriptions.

```javascript
import { signal, computed } from "@preact/signals-react";

// These live OUTSIDE components, they're module-level state
const todos = signal([]);
const filter = signal("all");

const filteredTodos = computed(() => {
 if (filter.value === "all") return todos.value;
 const done = filter.value === "done";
 return todos.value.filter(t => t.completed === done);
});

// This component re-renders ONLY when filteredTodos changes
function TodoList() {
 return (
 <ul>
 {filteredTodos.value.map(todo => (
 <TodoItem key={todo.id} todo={todo} />
 ))}
 </ul>
 );
}
```

I used this in a side project last year, a real-time trading dashboard, and the results were impressive. But there are sharp edges. The React adapter works by wrapping your components in a higher-order component that sets up signal tracking. This interacts poorly with:

- React DevTools (component names get mangled)
- React 19's compiler optimizations (the compiler assumes standard React semantics)
- Concurrent features like ` useTransition ` (signals are synchronous; transitions are not)

My take: Preact Signals in Preact is excellent. Preact Signals in React is a clever hack that's increasingly fragile as React evolves.

## The TC39 Proposal: Standardizing the Primitive

The TC39 Signals proposal (currently Stage 1, championed by Daniel Ehrenberg and others) aims to standardize a minimal signal primitive in JavaScript itself. Here's what the proposed API looks like:

```javascript
// Proposed standard API
const counter = new Signal.State(0);
const doubled = new Signal.Computed(() => counter.get() * 2);

// Reading
counter.get(); // 0
doubled.get(); // 0

// Writing
counter.set(5);
doubled.get(); // 10

// The Watcher API for scheduling
const watcher = new Signal.subtle.Watcher(() => {
 // Called synchronously when a watched signal is marked dirty
 // Frameworks use this to schedule their own rendering pass
 queueMicrotask(processPendingUpdates);
});
watcher.watch(doubled);
```

The key insight: the proposal deliberately excludes ` effect() `. Effects, side-effectful reactions to signal changes, are where frameworks diverge most. SolidJS effects are synchronous. Angular effects are scheduled. Vue's ` watchEffect ` batches in the microtask queue. The TC39 proposal provides ` Signal.subtle.Watcher ` as the low-level hook, letting each framework implement its own effect scheduling.

I've read through the proposal's polyfill implementation, and the dependency tracking uses a doubly-linked list between signal nodes. When a ` Computed ` evaluates, it captures reads via a global tracking context (similar to how SolidJS and Vue work internally):

```javascript
// Simplified signal tracking (conceptual)
let currentComputed = null;

class Signal {
 #value;
 #dependents = new Set();

 get() {
 // Track the dependency
 if (currentComputed) {
 this.#dependents.add(currentComputed);
 currentComputed.addSource(this);
 }
 return this.#value;
 }

 set(newVal) {
 if (Object.is(this.#value, newVal)) return;
 this.#value = newVal;
 // Notify dependents
 for (const dep of this.#dependents) {
 dep.markDirty();
 }
 }
}
```

## Benchmarks: Fine-Grained vs. Virtual DOM

I ran the JS Framework Benchmark suite on my workstation (Ryzen 9 7950X, 64GB DDR5, Chrome 123) across frameworks. Here are the results that matter, the ones that reflect real app patterns, not micro-benchmarks:

**Partial update (updating every 10th row in a 10,000 row table):**

| Framework | Time (ms) | Relative |
| --- | --- | --- |
| SolidJS 1.8 | 28.4 | 1.0x |
| Preact + Signals | 31.2 | 1.1x |
| Svelte 5 (runes) | 33.8 | 1.19x |
| Vue 3.4 | 45.1 | 1.59x |
| Angular 18 (signals) | 48.3 | 1.70x |
| React 19 | 67.8 | 2.39x |
| Angular 18 (zone.js) | 89.2 | 3.14x |

**Swap rows (swap 2 rows in a 1,000 row table):**

| Framework | Time (ms) | Relative |
| --- | --- | --- |
| SolidJS 1.8 | 12.1 | 1.0x |
| Svelte 5 (runes) | 13.4 | 1.11x |
| Vue 3.4 | 18.7 | 1.55x |
| React 19 | 22.3 | 1.84x |
| Angular 18 (signals) | 24.1 | 1.99x |

The pattern is consistent: frameworks with fine-grained reactivity (Solid, Svelte 5's runes, Preact+Signals) outperform virtual DOM frameworks on partial updates. The gap widens with larger component trees because the vDOM approach scales with tree size, while signals scale with change size.

## Where Signals Fall Short

I'd be dishonest if I didn't mention the tradeoffs.

**Debugging is harder.** In React, you can console.log at the top of a component and see every re-render. With signals, updates happen at the binding level, there's no component-level "this rendered" moment. SolidJS DevTools have improved, but they're nowhere near React DevTools maturity.

**Derived state diamonds.** Consider:

```javascript
const a = signal(1);
const b = computed(() => a.value + 1); // depends on a
const c = computed(() => a.value + 2); // depends on a
const d = computed(() => b.value + c.value); // depends on b and c
```

When ` a ` changes, both ` b ` and ` c ` are marked dirty. If ` d ` eagerly evaluates when ` b ` updates but before ` c ` updates, you get a "glitch", a transient inconsistent state. All serious signal implementations handle this (SolidJS uses topological sorting, Angular uses a push-pull algorithm), but it's a class of bug that doesn't exist in the "re-render everything" model.

**Server-side rendering.** Signals are inherently client-side primitives. SSR requires serializing signal values and hydrating them on the client, which every framework handles differently. SolidJS's approach (their ` renderToString ` captures initial values) works well, but it's more complex than React's "just render the component to HTML" model.

## My Take: The Industry is Converging

The broader trend is convergence, not competition. The industry is moving toward compiled reactivity:

- **React 19's compiler** analyzes your code at build time and inserts memoization automatically
- **Svelte 5's runes** compile signal-like syntax into fine-grained DOM operations
- **SolidJS** has always compiled JSX into direct DOM bindings
- **Vue Vapor** (in development) compiles templates into signal-based operations without the vDOM

The end state is the same: the framework figures out the minimal set of DOM mutations at compile time or through runtime tracking, and developers write straightforward declarative code. Signals are winning because they're the most natural runtime primitive for this, but the compiler is equally important.

If you're starting a new project today, my honest recommendation: SolidJS for maximum performance and a small bundle, Angular with signals for enterprise teams that need the full framework experience, Svelte 5 for the best developer experience. React is still a perfectly valid choice, the ecosystem is unmatched, but know that you're paying a runtime tax that other frameworks have eliminated.

The TC39 proposal won't ship for at least two more years. In the meantime, the pattern is clear: fine-grained reactivity, whether through signals or compiler magic, is the future. The virtual DOM was a brilliant innovation for its time. Its time is passing.

## Frequently asked questions

### Are signals just observables with a different name?

No. Observables are push-based streams that require manual subscription management. Signals are synchronous reactive primitives with automatic dependency tracking, the runtime builds a DAG at read-time, not subscribe-time. No teardown, no memory leaks from forgotten unsubscribes.

### Will the TC39 Signals proposal replace framework-specific implementations?

Eventually, partially. The TC39 proposal (Stage 1 as of early 2026) defines a minimal Signal primitive. Frameworks will still build their own scheduling, batching, and rendering integration on top. Think of it as a shared foundation, not a replacement.

### Should I rewrite my React app to use signals?

Probably not. React's compiler (React Forget / React 19) solves the re-render problem differently, through automatic memoization. Signals and compiled reactivity are converging on the same goal from different angles. Migration cost rarely justifies the perf gains unless you have measurable bottlenecks.

## Tips

1. **Tip:** When migrating Angular components to signals, start with ` computed() ` for derived state before touching ` effect() `, effects are the footgun, not signals themselves.
2. **Tip:** Preact Signals' ` @preact/signals-react ` adapter works in React, but watch out, it monkey-patches ` React.createElement `, which breaks with React 19's compiler optimizations.
3. **Tip:** In SolidJS, never destructure props in component arguments. ` const MyComp = (props) => <div>{props.name}</div> ` is reactive; ` const MyComp = ({name}) => <div>{name}</div> ` kills reactivity dead.

- signals
- reactivity
- angular
- solidjs
- preact
- tc39
- virtual-dom
- fine-grained-reactivity
- frontend-architecture

[Back to Blog](<https://lucioduran.com/blog>)
