{"schemaVersion":1,"id":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact","title":"Signals and Fine-Grained Reactivity: Angular, SolidJS, Preact, and the Shift from Virtual DOM Diffing","description":"Fine-grained reactivity is winning. I traced the signal pattern from SolidJS through Angular 17+ and Preact Signals, dug into the TC39 proposal, and benchmarked why virtual DOM diffing is becoming an expensive relic.","language":"en","datePublished":"2026-02-01","dateModified":null,"contentFormat":"text/markdown","content":"Frontend\n\n# Signals and Fine-Grained Reactivity: Angular, SolidJS, Preact, and the Shift from Virtual DOM Diffing\n\n2026-02-019 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact>)\n\n## The Cost of Virtual DOM Diffing\n\nReact 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:\n\n1. Re-executing your entire component function on every state change\n2. Producing a new virtual DOM tree\n3. Running a recursive O(n) diff against the previous tree\n4. Batching the resulting DOM patches\n\nThe 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.\n\n```javascript\n// This component re-renders when ANY part of appState changes\nfunction Dashboard({ appState }) {\n return (\n <div>\n <Header user={appState.user} />\n <Sidebar notifications={appState.notifications} />\n <MainContent data={appState.data} />\n {/* If appState.notifications changes, MainContent\n still re-executes. The diff catches that its output\n hasn't changed, but the function already ran. */}\n </div>\n );\n}\n```\n\nReact'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).\n\n## Enter Signals: Surgical Reactivity\n\nSignals 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.\n\nHere is what [SolidJS](<https://www.solidjs.com/>) does internally when you write:\n\n```javascript\nimport { createSignal, createEffect } from \"solid-js\";\n\nconst [count, setCount] = createSignal(0);\nconst doubled = () => count() * 2;\n\ncreateEffect(() => {\n // Solid tracks that this effect reads `count`\n // When count changes, ONLY this effect re-runs\n console.log(\"Count is:\", count());\n});\n\n// In a component:\nfunction Counter() {\n const [count, setCount] = createSignal(0);\n\n // This JSX compiles to direct DOM operations.\n // The {count()} expression becomes a fine-grained\n // subscription that updates ONE text node.\n return (\n <div>\n <span>{count()}</span>\n <button onClick={() => setCount(c => c + 1)}>+</button>\n </div>\n );\n}\n```\n\nThe compilation output looks roughly like this (simplified):\n\n```javascript\nfunction Counter() {\n const [count, setCount] = createSignal(0);\n\n const _el = document.createElement(\"div\");\n const _span = document.createElement(\"span\");\n const _text = document.createTextNode(\"\");\n const _btn = document.createElement(\"button\");\n\n // Direct subscription: signal -> text node\n createRenderEffect(() => {\n _text.data = count();\n });\n\n _btn.addEventListener(\"click\", () => setCount(c => c + 1));\n _span.appendChild(_text);\n _el.appendChild(_span);\n _el.appendChild(_btn);\n return _el;\n}\n```\n\nNo 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.\n\n## Angular Signals: Incremental Adoption\n\n[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.\n\n```typescript\nimport { Component, signal, computed, effect } from '@angular/core';\n\n@Component({\n selector: 'app-inventory',\n template: `\n <div>\n <p>Items: {{ items().length }}</p>\n <p>Total value: {{ totalValue() }}</p>\n <ul>\n @for (item of filteredItems(); track item.id) {\n <li>{{ item.name }} - {{ item.price | currency }}</li>\n }\n </ul>\n </div>\n `\n})\nexport class InventoryComponent {\n items = signal<Item[]>([]);\n filter = signal('');\n\n filteredItems = computed(() => {\n const f = this.filter().toLowerCase();\n return this.items().filter(i =>\n i.name.toLowerCase().includes(f)\n );\n });\n\n totalValue = computed(() =>\n this.filteredItems().reduce((sum, i) => sum + i.price, 0)\n );\n\n constructor() {\n effect(() => {\n // Automatic dependency tracking, runs when\n // filteredItems() changes, not when unrelated\n // signals change\n console.log(`Showing ${this.filteredItems().length} items`);\n });\n }\n}\n```\n\nWhat 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.\n\nThe 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.\n\n## Preact Signals: The Trojan Horse\n\n[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.\n\n```javascript\nimport { signal, computed } from \"@preact/signals-react\";\n\n// These live OUTSIDE components, they're module-level state\nconst todos = signal([]);\nconst filter = signal(\"all\");\n\nconst filteredTodos = computed(() => {\n if (filter.value === \"all\") return todos.value;\n const done = filter.value === \"done\";\n return todos.value.filter(t => t.completed === done);\n});\n\n// This component re-renders ONLY when filteredTodos changes\nfunction TodoList() {\n return (\n <ul>\n {filteredTodos.value.map(todo => (\n <TodoItem key={todo.id} todo={todo} />\n ))}\n </ul>\n );\n}\n```\n\nI 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:\n\n- React DevTools (component names get mangled)\n- React 19's compiler optimizations (the compiler assumes standard React semantics)\n- Concurrent features like ` useTransition ` (signals are synchronous; transitions are not)\n\nMy take: Preact Signals in Preact is excellent. Preact Signals in React is a clever hack that's increasingly fragile as React evolves.\n\n## The TC39 Proposal: Standardizing the Primitive\n\nThe 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:\n\n```javascript\n// Proposed standard API\nconst counter = new Signal.State(0);\nconst doubled = new Signal.Computed(() => counter.get() * 2);\n\n// Reading\ncounter.get(); // 0\ndoubled.get(); // 0\n\n// Writing\ncounter.set(5);\ndoubled.get(); // 10\n\n// The Watcher API for scheduling\nconst watcher = new Signal.subtle.Watcher(() => {\n // Called synchronously when a watched signal is marked dirty\n // Frameworks use this to schedule their own rendering pass\n queueMicrotask(processPendingUpdates);\n});\nwatcher.watch(doubled);\n```\n\nThe 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.\n\nI'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):\n\n```javascript\n// Simplified signal tracking (conceptual)\nlet currentComputed = null;\n\nclass Signal {\n #value;\n #dependents = new Set();\n\n get() {\n // Track the dependency\n if (currentComputed) {\n this.#dependents.add(currentComputed);\n currentComputed.addSource(this);\n }\n return this.#value;\n }\n\n set(newVal) {\n if (Object.is(this.#value, newVal)) return;\n this.#value = newVal;\n // Notify dependents\n for (const dep of this.#dependents) {\n dep.markDirty();\n }\n }\n}\n```\n\n## Benchmarks: Fine-Grained vs. Virtual DOM\n\nI 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:\n\n**Partial update (updating every 10th row in a 10,000 row table):**\n\n| Framework | Time (ms) | Relative |\n| --- | --- | --- |\n| SolidJS 1.8 | 28.4 | 1.0x |\n| Preact + Signals | 31.2 | 1.1x |\n| Svelte 5 (runes) | 33.8 | 1.19x |\n| Vue 3.4 | 45.1 | 1.59x |\n| Angular 18 (signals) | 48.3 | 1.70x |\n| React 19 | 67.8 | 2.39x |\n| Angular 18 (zone.js) | 89.2 | 3.14x |\n\n**Swap rows (swap 2 rows in a 1,000 row table):**\n\n| Framework | Time (ms) | Relative |\n| --- | --- | --- |\n| SolidJS 1.8 | 12.1 | 1.0x |\n| Svelte 5 (runes) | 13.4 | 1.11x |\n| Vue 3.4 | 18.7 | 1.55x |\n| React 19 | 22.3 | 1.84x |\n| Angular 18 (signals) | 24.1 | 1.99x |\n\nThe 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.\n\n## Where Signals Fall Short\n\nI'd be dishonest if I didn't mention the tradeoffs.\n\n**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.\n\n**Derived state diamonds.** Consider:\n\n```javascript\nconst a = signal(1);\nconst b = computed(() => a.value + 1); // depends on a\nconst c = computed(() => a.value + 2); // depends on a\nconst d = computed(() => b.value + c.value); // depends on b and c\n```\n\nWhen ` 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.\n\n**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.\n\n## My Take: The Industry is Converging\n\nThe broader trend is convergence, not competition. The industry is moving toward compiled reactivity:\n\n- **React 19's compiler** analyzes your code at build time and inserts memoization automatically\n- **Svelte 5's runes** compile signal-like syntax into fine-grained DOM operations\n- **SolidJS** has always compiled JSX into direct DOM bindings\n- **Vue Vapor** (in development) compiles templates into signal-based operations without the vDOM\n\nThe 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.\n\nIf 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.\n\nThe 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.\n\n## Frequently asked questions\n\n### Are signals just observables with a different name?\n\nNo. 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.\n\n### Will the TC39 Signals proposal replace framework-specific implementations?\n\nEventually, 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.\n\n### Should I rewrite my React app to use signals?\n\nProbably 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.\n\n## Tips\n\n1. **Tip:** When migrating Angular components to signals, start with ` computed() ` for derived state before touching ` effect() `, effects are the footgun, not signals themselves.\n2. **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.\n3. **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.\n\n- signals\n- reactivity\n- angular\n- solidjs\n- preact\n- tc39\n- virtual-dom\n- fine-grained-reactivity\n- frontend-architecture\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Signals and Fine-Grained Reactivity: Angular, SolidJS, Preact, and the Shift from Virtual DOM Diffing","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#title"},{"level":2,"text":"The Cost of Virtual DOM Diffing","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#the-cost-of-virtual-dom-diffing"},{"level":2,"text":"Enter Signals: Surgical Reactivity","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#enter-signals-surgical-reactivity"},{"level":2,"text":"Angular Signals: Incremental Adoption","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#angular-signals-incremental-adoption"},{"level":2,"text":"Preact Signals: The Trojan Horse","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#preact-signals-the-trojan-horse"},{"level":2,"text":"The TC39 Proposal: Standardizing the Primitive","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#the-tc39-proposal-standardizing-the-primitive"},{"level":2,"text":"Benchmarks: Fine-Grained vs. Virtual DOM","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#benchmarks-fine-grained-vs-virtual-dom"},{"level":2,"text":"Where Signals Fall Short","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#where-signals-fall-short"},{"level":2,"text":"My Take: The Industry is Converging","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#my-take-the-industry-is-converging"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#questions"},{"level":3,"text":"Are signals just observables with a different name?","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#question-1"},{"level":3,"text":"Will the TC39 Signals proposal replace framework-specific implementations?","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#question-2"},{"level":3,"text":"Should I rewrite my React app to use signals?","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#article-tips"}],"sections":[{"heading":"Signals and Fine-Grained Reactivity: Angular, SolidJS, Preact, and the Shift from Virtual DOM Diffing","level":1,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact","content":"Frontend"},{"heading":"Signals and Fine-Grained Reactivity: Angular, SolidJS, Preact, and the Shift from Virtual DOM Diffing","level":1,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#title","content":"2026-02-019 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact>)"},{"heading":"The Cost of Virtual DOM Diffing","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#the-cost-of-virtual-dom-diffing","content":"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:\n\n1. Re-executing your entire component function on every state change\n2. Producing a new virtual DOM tree\n3. Running a recursive O(n) diff against the previous tree\n4. Batching the resulting DOM patches\n\nThe 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.\n\n```javascript\n// This component re-renders when ANY part of appState changes\nfunction Dashboard({ appState }) {\n return (\n <div>\n <Header user={appState.user} />\n <Sidebar notifications={appState.notifications} />\n <MainContent data={appState.data} />\n {/* If appState.notifications changes, MainContent\n still re-executes. The diff catches that its output\n hasn't changed, but the function already ran. */}\n </div>\n );\n}\n```\n\nReact'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)."},{"heading":"Enter Signals: Surgical Reactivity","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#enter-signals-surgical-reactivity","content":"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.\n\nHere is what [SolidJS](<https://www.solidjs.com/>) does internally when you write:\n\n```javascript\nimport { createSignal, createEffect } from \"solid-js\";\n\nconst [count, setCount] = createSignal(0);\nconst doubled = () => count() * 2;\n\ncreateEffect(() => {\n // Solid tracks that this effect reads `count`\n // When count changes, ONLY this effect re-runs\n console.log(\"Count is:\", count());\n});\n\n// In a component:\nfunction Counter() {\n const [count, setCount] = createSignal(0);\n\n // This JSX compiles to direct DOM operations.\n // The {count()} expression becomes a fine-grained\n // subscription that updates ONE text node.\n return (\n <div>\n <span>{count()}</span>\n <button onClick={() => setCount(c => c + 1)}>+</button>\n </div>\n );\n}\n```\n\nThe compilation output looks roughly like this (simplified):\n\n```javascript\nfunction Counter() {\n const [count, setCount] = createSignal(0);\n\n const _el = document.createElement(\"div\");\n const _span = document.createElement(\"span\");\n const _text = document.createTextNode(\"\");\n const _btn = document.createElement(\"button\");\n\n // Direct subscription: signal -> text node\n createRenderEffect(() => {\n _text.data = count();\n });\n\n _btn.addEventListener(\"click\", () => setCount(c => c + 1));\n _span.appendChild(_text);\n _el.appendChild(_span);\n _el.appendChild(_btn);\n return _el;\n}\n```\n\nNo 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."},{"heading":"Angular Signals: Incremental Adoption","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#angular-signals-incremental-adoption","content":"[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.\n\n```typescript\nimport { Component, signal, computed, effect } from '@angular/core';\n\n@Component({\n selector: 'app-inventory',\n template: `\n <div>\n <p>Items: {{ items().length }}</p>\n <p>Total value: {{ totalValue() }}</p>\n <ul>\n @for (item of filteredItems(); track item.id) {\n <li>{{ item.name }} - {{ item.price | currency }}</li>\n }\n </ul>\n </div>\n `\n})\nexport class InventoryComponent {\n items = signal<Item[]>([]);\n filter = signal('');\n\n filteredItems = computed(() => {\n const f = this.filter().toLowerCase();\n return this.items().filter(i =>\n i.name.toLowerCase().includes(f)\n );\n });\n\n totalValue = computed(() =>\n this.filteredItems().reduce((sum, i) => sum + i.price, 0)\n );\n\n constructor() {\n effect(() => {\n // Automatic dependency tracking, runs when\n // filteredItems() changes, not when unrelated\n // signals change\n console.log(`Showing ${this.filteredItems().length} items`);\n });\n }\n}\n```\n\nWhat 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.\n\nThe 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."},{"heading":"Preact Signals: The Trojan Horse","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#preact-signals-the-trojan-horse","content":"[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.\n\n```javascript\nimport { signal, computed } from \"@preact/signals-react\";\n\n// These live OUTSIDE components, they're module-level state\nconst todos = signal([]);\nconst filter = signal(\"all\");\n\nconst filteredTodos = computed(() => {\n if (filter.value === \"all\") return todos.value;\n const done = filter.value === \"done\";\n return todos.value.filter(t => t.completed === done);\n});\n\n// This component re-renders ONLY when filteredTodos changes\nfunction TodoList() {\n return (\n <ul>\n {filteredTodos.value.map(todo => (\n <TodoItem key={todo.id} todo={todo} />\n ))}\n </ul>\n );\n}\n```\n\nI 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:\n\n- React DevTools (component names get mangled)\n- React 19's compiler optimizations (the compiler assumes standard React semantics)\n- Concurrent features like ` useTransition ` (signals are synchronous; transitions are not)\n\nMy take: Preact Signals in Preact is excellent. Preact Signals in React is a clever hack that's increasingly fragile as React evolves."},{"heading":"The TC39 Proposal: Standardizing the Primitive","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#the-tc39-proposal-standardizing-the-primitive","content":"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:\n\n```javascript\n// Proposed standard API\nconst counter = new Signal.State(0);\nconst doubled = new Signal.Computed(() => counter.get() * 2);\n\n// Reading\ncounter.get(); // 0\ndoubled.get(); // 0\n\n// Writing\ncounter.set(5);\ndoubled.get(); // 10\n\n// The Watcher API for scheduling\nconst watcher = new Signal.subtle.Watcher(() => {\n // Called synchronously when a watched signal is marked dirty\n // Frameworks use this to schedule their own rendering pass\n queueMicrotask(processPendingUpdates);\n});\nwatcher.watch(doubled);\n```\n\nThe 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.\n\nI'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):\n\n```javascript\n// Simplified signal tracking (conceptual)\nlet currentComputed = null;\n\nclass Signal {\n #value;\n #dependents = new Set();\n\n get() {\n // Track the dependency\n if (currentComputed) {\n this.#dependents.add(currentComputed);\n currentComputed.addSource(this);\n }\n return this.#value;\n }\n\n set(newVal) {\n if (Object.is(this.#value, newVal)) return;\n this.#value = newVal;\n // Notify dependents\n for (const dep of this.#dependents) {\n dep.markDirty();\n }\n }\n}\n```"},{"heading":"Benchmarks: Fine-Grained vs. Virtual DOM","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#benchmarks-fine-grained-vs-virtual-dom","content":"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:\n\n**Partial update (updating every 10th row in a 10,000 row table):**\n\n| Framework | Time (ms) | Relative |\n| --- | --- | --- |\n| SolidJS 1.8 | 28.4 | 1.0x |\n| Preact + Signals | 31.2 | 1.1x |\n| Svelte 5 (runes) | 33.8 | 1.19x |\n| Vue 3.4 | 45.1 | 1.59x |\n| Angular 18 (signals) | 48.3 | 1.70x |\n| React 19 | 67.8 | 2.39x |\n| Angular 18 (zone.js) | 89.2 | 3.14x |\n\n**Swap rows (swap 2 rows in a 1,000 row table):**\n\n| Framework | Time (ms) | Relative |\n| --- | --- | --- |\n| SolidJS 1.8 | 12.1 | 1.0x |\n| Svelte 5 (runes) | 13.4 | 1.11x |\n| Vue 3.4 | 18.7 | 1.55x |\n| React 19 | 22.3 | 1.84x |\n| Angular 18 (signals) | 24.1 | 1.99x |\n\nThe 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."},{"heading":"Where Signals Fall Short","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#where-signals-fall-short","content":"I'd be dishonest if I didn't mention the tradeoffs.\n\n**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.\n\n**Derived state diamonds.** Consider:\n\n```javascript\nconst a = signal(1);\nconst b = computed(() => a.value + 1); // depends on a\nconst c = computed(() => a.value + 2); // depends on a\nconst d = computed(() => b.value + c.value); // depends on b and c\n```\n\nWhen ` 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.\n\n**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."},{"heading":"My Take: The Industry is Converging","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#my-take-the-industry-is-converging","content":"The broader trend is convergence, not competition. The industry is moving toward compiled reactivity:\n\n- **React 19's compiler** analyzes your code at build time and inserts memoization automatically\n- **Svelte 5's runes** compile signal-like syntax into fine-grained DOM operations\n- **SolidJS** has always compiled JSX into direct DOM bindings\n- **Vue Vapor** (in development) compiles templates into signal-based operations without the vDOM\n\nThe 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.\n\nIf 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.\n\nThe 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."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#questions","content":""},{"heading":"Are signals just observables with a different name?","level":3,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#question-1","content":"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."},{"heading":"Will the TC39 Signals proposal replace framework-specific implementations?","level":3,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#question-2","content":"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."},{"heading":"Should I rewrite my React app to use signals?","level":3,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#question-3","content":"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."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#article-tips","content":"1. **Tip:** When migrating Angular components to signals, start with ` computed() ` for derived state before touching ` effect() `, effects are the footgun, not signals themselves.\n2. **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.\n3. **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.\n\n- signals\n- reactivity\n- angular\n- solidjs\n- preact\n- tc39\n- virtual-dom\n- fine-grained-reactivity\n- frontend-architecture\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact"},{"text":"SolidJS","url":"https://www.solidjs.com/"},{"text":"Angular","url":"https://angular.dev/"},{"text":"Preact","url":"https://preactjs.com/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact"},"headline":"Signals and Fine-Grained Reactivity: Angular, SolidJS, Preact, and the Shift from Virtual DOM Diffing","description":"Fine-grained reactivity is winning. I traced the signal pattern from SolidJS through Angular 17+ and Preact Signals, dug into the TC39 proposal, and benchmarked why virtual DOM diffing is becoming an expensive relic.","datePublished":"2026-02-01","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Frontend","keywords":"signals, reactivity, angular, solidjs, preact, tc39, virtual-dom, fine-grained-reactivity, frontend-architecture","wordCount":1705,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Signals and Fine-Grained Reactivity: Angular, SolidJS, Preact, and the Shift from Virtual DOM Diffing","item":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Are signals just observables with a different name?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Will the TC39 Signals proposal replace framework-specific implementations?","acceptedAnswer":{"@type":"Answer","text":"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."}},{"@type":"Question","name":"Should I rewrite my React app to use signals?","acceptedAnswer":{"@type":"Answer","text":"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."}}]}]}],"contentHash":"3cf742baa1f37a212c73d715ccd325132fd7f78831e3e20e2ba6a4e0e7261d7c","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":70656,"contentBytes":14692}}
