{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact","title":"Signals y Reactividad Fine-Grained: Angular, SolidJS, Preact y la Transición del Diffing de Virtual DOM","description":"La reactividad fine-grained se está imponiendo. Análisis del patrón de signals desde SolidJS pasando por Angular 17+ y Preact Signals, la propuesta TC39, y benchmarks que muestran por qué el diffing de virtual DOM se está convirtiendo en un costo innecesario.","language":"es","datePublished":"2026-02-01","dateModified":null,"contentFormat":"text/markdown","content":"Frontend\n\n# Signals y Reactividad Fine-Grained: Angular, SolidJS, Preact y la Transición del Diffing de Virtual DOM\n\n2026-02-0110 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/signals-reactivity-angular-solid-preact>)\n\n## El Costo del Diffing de Virtual DOM\n\nReact popularizó el virtual DOM en 2013 con una propuesta elegante: declarar la UI como una función del estado, y el framework se encarga de las mutaciones mínimas del DOM. En teoría es elegante. En la práctica, \"encargarse de las mutaciones mínimas\" significa:\n\n1. Re-ejecutar toda tu función de componente en cada cambio de estado\n2. Producir un árbol virtual DOM nuevo\n3. Correr un diff recursivo O(n) contra el árbol anterior\n4. Agrupar los patches de DOM resultantes\n\nEl punto crítico es el paso 1. Cuando un pedazo de estado cambia en un componente padre, React re-ejecuta *todos los componentes hijos* en ese subárbol por defecto. El algoritmo de diff es rápido, sí, pero se está diffeando árboles que son 95% idénticos porque solo una hoja cambió.\n\n```javascript\n// Este componente se re-renderiza cuando CUALQUIER parte\n// de appState cambia\nfunction Dashboard({ appState }) {\n return (\n <div>\n <Header user={appState.user} />\n <Sidebar notifications={appState.notifications} />\n <MainContent data={appState.data} />\n {/* Si appState.notifications cambia, MainContent\n igual se re-ejecuta. El diff capta que su output\n no cambió, pero la función ya corrió. */}\n </div>\n );\n}\n```\n\nLa respuesta de React fue ` React.memo `, ` useMemo `, ` useCallback `, hints de memoización manual que los desarrolladores consistentemente la embarran. Revisé codebases de producción donde usaban ` useMemo ` en valores primitivos (innecesariamente) y lo omitían en computaciones caras (anulando el propósito).\n\n## Entran los Signals: Reactividad Quirúrgica\n\nLos signals invierten el modelo. En vez de re-rendering top-down con diffing bottom-up, los signals trackean dependencias al momento de lectura y actualizan únicamente los nodos DOM específicos que dependen de los valores que cambiaron.\n\nA continuación se muestra lo que [SolidJS](<https://www.solidjs.com/>) hace internamente cuando escribirs:\n\n```javascript\nimport { createSignal, createEffect } from \"solid-js\";\n\nconst [count, setCount] = createSignal(0);\nconst doubled = () => count() * 2;\n\ncreateEffect(() => {\n // Solid trackea que este effect lee `count`\n // Cuando count cambia, SOLO este effect se re-ejecuta\n console.log(\"Count es:\", count());\n});\n\n// En un componente:\nfunction Counter() {\n const [count, setCount] = createSignal(0);\n\n // Este JSX se compila a operaciones DOM directas.\n // La expresión {count()} se convierte en una\n // suscripción fine-grained que actualiza UN text node.\n return (\n <div>\n <span>{count()}</span>\n <button onClick={() => setCount(c => c + 1)}>+</button>\n </div>\n );\n}\n```\n\nLa salida de compilación se ve más o menos así (simplificado):\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 // Suscripción directa: 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\nSin virtual DOM. Sin diffing. Sin re-ejecución de componentes. El signal escribe directo al text node. Perfilé esto en un dashboard real que mantengo, un panel de monitoreo con ~400 valores actualizándose, y la diferencia fue notable. React con memoización correcta: ~12ms por ciclo de actualización. SolidJS: ~0.8ms. Esto no es un juego de benchmarks; es una app de producción renderizando datos reales de WebSocket.\n\n## Angular Signals: La Revolución Incremental\n\nEl enfoque de Angular es interesante porque no podían volar por el aire su modelo existente. Tenían que hacer signals opt-in e interoperable con la detección de cambios basada en zone.js.\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>Valor total: {{ 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 console.log(`Mostrando ${this.filteredItems().length} items`);\n });\n }\n}\n```\n\nLo que me parece notable de la implementación de Angular es el primitivo ` computed() `. Es lazy, no recalcula hasta que lo leés, y cachea su valor. Si ` filter ` cambia pero produce el mismo resultado de ` filteredItems `, ` totalValue ` ni se inmuta. Esa garantía libre de glitches importa en grafos de dependencias complejos.\n\nLa implementación interna usa un counter de versión en cada nodo signal. Cuando llamás ` signal.set() `, incrementa la versión. Los signals computed chequean las versiones upstream antes de recalcular, si nada cambió, devuelven el valor cacheado sin recorrer el grafo.\n\n## Preact Signals: El Caballo de Troya\n\nPreact Signals hizo algo audaz: hicieron que signals funcionen dentro de React. El paquete ` @preact/signals-react ` intercepta el rendering de componentes y reemplaza las lecturas de signals con suscripciones fine-grained.\n\n```javascript\nimport { signal, computed } from \"@preact/signals-react\";\n\n// Estos viven FUERA de los componentes, son estado a nivel módulo\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// Este componente se re-renderiza SOLO cuando filteredTodos cambia\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\nUsé esto en un proyecto personal el año pasado, un dashboard de trading en tiempo real, y los resultados fueron impresionantes. Pero hay filos cortantes. El adapter de React funciona wrapeando tus componentes en un HOC que configura tracking de signals. Esto interactúa mal con:\n\n- React DevTools (los nombres de componentes quedan mangled)\n- Las optimizaciones del compiler de React 19 (el compiler asume semántica React estándar)\n- Features concurrentes como ` useTransition ` (signals son sincrónicos; transitions no)\n\nMi opinión: Preact Signals en Preact es excelente. Preact Signals en React es un hack ingenioso que se vuelve cada vez más frágil a medida que React evoluciona.\n\n## La Propuesta TC39: Estandarizando el Primitivo\n\nLa propuesta TC39 de Signals (actualmente Stage 1, liderada por Daniel Ehrenberg y otros) busca estandarizar un primitivo signal mínimo en JavaScript mismo. Así se ve la API propuesta:\n\n```javascript\n// API estándar propuesta\nconst counter = new Signal.State(0);\nconst doubled = new Signal.Computed(() => counter.get() * 2);\n\n// Lectura\ncounter.get(); // 0\ndoubled.get(); // 0\n\n// Escritura\ncounter.set(5);\ndoubled.get(); // 10\n\n// La API Watcher para scheduling\nconst watcher = new Signal.subtle.Watcher(() => {\n // Se llama sincrónicamente cuando un signal observado\n // se marca como dirty\n // Los frameworks usan esto para agendar su propio\n // pase de rendering\n queueMicrotask(processPendingUpdates);\n});\nwatcher.watch(doubled);\n```\n\nEl insight clave: la propuesta deliberadamente excluye ` effect() `. Los effects, reacciones con side-effects a cambios de signals, son donde los frameworks más divergen. Los effects de SolidJS son sincrónicos. Los de Angular son agendados. El ` watchEffect ` de Vue batchea en la cola de microtasks. La propuesta TC39 provee ` Signal.subtle.Watcher ` como hook de bajo nivel, dejando que cada framework implemente su propio scheduling de effects.\n\nLeí la implementación del polyfill de la propuesta, y el tracking de dependencias usa una lista doblemente enlazada entre nodos signal. Cuando un ` Computed ` evalúa, captura lecturas vía un contexto de tracking global (similar a como funcionan internamente SolidJS y Vue):\n\n```javascript\n// Tracking de signals simplificado (conceptual)\nlet currentComputed = null;\n\nclass Signal {\n #value;\n #dependents = new Set();\n\n get() {\n // Trackear la dependencia\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 // Notificar dependientes\n for (const dep of this.#dependents) {\n dep.markDirty();\n }\n }\n}\n```\n\n## Benchmarks: Fine-Grained vs. Virtual DOM\n\nCorrí la suite JS Framework Benchmark en mi workstation (Ryzen 9 7950X, 64GB DDR5, Chrome 123) entre frameworks. Acá van los resultados que importan, los que reflejan patrones reales de apps, no micro-benchmarks:\n\n**Actualización parcial (actualizando cada 10ma fila en una tabla de 10.000 filas):**\n\n| Framework | Tiempo (ms) | Relativo |\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**Intercambio de filas (intercambiar 2 filas en una tabla de 1.000 filas):**\n\n| Framework | Tiempo (ms) | Relativo |\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\nEl patrón es consistente: los frameworks con reactividad fine-grained (Solid, runes de Svelte 5, Preact+Signals) superan a los frameworks de virtual DOM en actualizaciones parciales. La brecha se amplía con árboles de componentes más grandes porque el enfoque vDOM escala con el tamaño del árbol, mientras que signals escala con el tamaño del cambio.\n\n## Donde los Signals se Quedan Cortos\n\nSería deshonesto si no mencionara los tradeoffs.\n\n**Debuggear es más difícil.** En React, es posible meter un console.log arriba del componente y ver cada re-render. Con signals, las actualizaciones pasan a nivel de binding, no hay un momento \"esto se renderizó\" a nivel componente. Las DevTools de SolidJS han mejorado, pero aún no alcanzan el nivel de madurez de las React DevTools.\n\n**Diamantes de estado derivado.** Considerá:\n\n```javascript\nconst a = signal(1);\nconst b = computed(() => a.value + 1); // depende de a\nconst c = computed(() => a.value + 2); // depende de a\nconst d = computed(() => b.value + c.value); // depende de b y c\n```\n\nCuando ` a ` cambia, tanto ` b ` como ` c ` se marcan dirty. Si ` d ` evalúa eagerly cuando ` b ` se actualiza pero antes de que ` c ` se actualice, teners un \"glitch\", un estado transitorio inconsistente. Todas las implementaciones serias de signals manejan esto (SolidJS usa ordenamiento topológico, Angular usa un algoritmo push-pull), pero es una clase de bug que no existe en el modelo \"re-renderizar todo\".\n\n**Server-side rendering.** Los signals son inherentemente primitivos del cliente. SSR requiere serializar valores de signals e hidratarlos en el cliente, cosa que cada framework maneja distinto. El approach de SolidJS (su ` renderToString ` captura valores iniciales) funciona bien, pero es más complejo que el modelo \"renderizar el componente a HTML\" de React.\n\n## Mi Opinión: La Industria Está Convergiendo\n\nEsto es lo que creo que está pasando realmente. No estamos en una \"guerra signals vs. virtual DOM\". Estamos viendo a la industria converger en reactividad compilada:\n\n- **El compiler de React 19** analiza tu código en build time e inserta memoización automáticamente\n- **Las runes de Svelte 5** compilan sintaxis tipo signal en operaciones DOM fine-grained\n- **SolidJS** siempre compiló JSX en bindings DOM directos\n- **Vue Vapor** (en desarrollo) compila templates en operaciones basadas en signals sin vDOM\n\nEl estado final es el mismo: el framework deduce el set mínimo de mutaciones DOM en compile time o a través de tracking en runtime, y los desarrolladores escriben código declarativo directo. Los signals están ganando porque son el primitivo runtime más natural para esto, pero el compiler es igual de importante.\n\nSi se está arrancando un proyecto nuevo hoy, mi recomendación honesta: SolidJS para máxima performance y bundle chico, Angular con signals para equipos enterprise que necesitan la experiencia full-framework, Svelte 5 para la mejor developer experience. React sigue siendo una opción perfectamente válida, el ecosistema no tiene rival, pero sabé que se está pagando un impuesto runtime que otros frameworks ya eliminaron.\n\nLa propuesta TC39 no va a salir por lo menos en dos años más. Mientras tanto, el patrón es claro: reactividad fine-grained, ya sea a través de signals o magia del compiler, es el futuro. El virtual DOM fue una innovación brillante para su época. Su época está pasando.\n\n## Preguntas frecuentes\n\n### ¿Los signals son observables con otro nombre?\n\nNo. Los observables son streams push-based que requieren manejo manual de suscripciones. Los signals son primitivos reactivos sincrónicos con tracking automático de dependencias, el runtime arma un DAG en el momento de lectura, no de suscripción. Sin teardown, sin memory leaks por unsubscribes olvidados.\n\n### ¿La propuesta TC39 de Signals va a reemplazar las implementaciones de cada framework?\n\nEventualmente, parcialmente. La propuesta TC39 (Stage 1 a principios de 2026) define un primitivo Signal mínimo. Los frameworks van a seguir armando su propio scheduling, batching e integración de rendering encima. Pensalo como un cimiento compartido, no un reemplazo.\n\n### ¿Debería reescribir mi app React para usar signals?\n\nProbablemente no. El compiler de React (React Forget / React 19) resuelve el problema de re-renders de otra manera, mediante memoización automática. Signals y reactividad compilada están convergiendo al mismo objetivo desde ángulos distintos. El costo de migración rara vez justifica las mejoras de performance salvo que tengas cuellos de botella medibles.\n\n## Tips\n\n1. **Tip:** Al migrar componentes Angular a signals, comenzar con ` computed() ` para estado derivado antes de utilizar ` effect() `, los effects son el punto de riesgo, no los signals en sí.\n2. **Tip:** El adapter ` @preact/signals-react ` funciona en React, pero con precaución, aplica monkey-patch a ` React.createElement `, lo que genera conflictos con las optimizaciones del compiler de React 19.\n3. **Tip:** En SolidJS, nunca desestructurar props en los argumentos del componente. ` const MyComp = (props) => <div>{props.name}</div> ` es reactivo; ` const MyComp = ({name}) => <div>{name}</div> ` elimina la reactividad por completo.\n\n- signals\n- reactividad\n- angular\n- solidjs\n- preact\n- tc39\n- virtual-dom\n- reactividad-fine-grained\n- arquitectura-frontend\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"Signals y Reactividad Fine-Grained: Angular, SolidJS, Preact y la Transición del Diffing de Virtual DOM","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#title"},{"level":2,"text":"El Costo del Diffing de Virtual DOM","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#el-costo-del-diffing-de-virtual-dom"},{"level":2,"text":"Entran los Signals: Reactividad Quirúrgica","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#entran-los-signals-reactividad-quirrgica"},{"level":2,"text":"Angular Signals: La Revolución Incremental","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#angular-signals-la-revolucin-incremental"},{"level":2,"text":"Preact Signals: El Caballo de Troya","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#preact-signals-el-caballo-de-troya"},{"level":2,"text":"La Propuesta TC39: Estandarizando el Primitivo","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#la-propuesta-tc39-estandarizando-el-primitivo"},{"level":2,"text":"Benchmarks: Fine-Grained vs. Virtual DOM","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#benchmarks-fine-grained-vs-virtual-dom"},{"level":2,"text":"Donde los Signals se Quedan Cortos","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#donde-los-signals-se-quedan-cortos"},{"level":2,"text":"Mi Opinión: La Industria Está Convergiendo","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#mi-opinin-la-industria-est-convergiendo"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#questions"},{"level":3,"text":"¿Los signals son observables con otro nombre?","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#question-1"},{"level":3,"text":"¿La propuesta TC39 de Signals va a reemplazar las implementaciones de cada framework?","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#question-2"},{"level":3,"text":"¿Debería reescribir mi app React para usar signals?","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#article-tips"}],"sections":[{"heading":"Signals y Reactividad Fine-Grained: Angular, SolidJS, Preact y la Transición del Diffing de Virtual DOM","level":1,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact","content":"Frontend"},{"heading":"Signals y Reactividad Fine-Grained: Angular, SolidJS, Preact y la Transición del Diffing de Virtual DOM","level":1,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#title","content":"2026-02-0110 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/signals-reactivity-angular-solid-preact>)"},{"heading":"El Costo del Diffing de Virtual DOM","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#el-costo-del-diffing-de-virtual-dom","content":"React popularizó el virtual DOM en 2013 con una propuesta elegante: declarar la UI como una función del estado, y el framework se encarga de las mutaciones mínimas del DOM. En teoría es elegante. En la práctica, \"encargarse de las mutaciones mínimas\" significa:\n\n1. Re-ejecutar toda tu función de componente en cada cambio de estado\n2. Producir un árbol virtual DOM nuevo\n3. Correr un diff recursivo O(n) contra el árbol anterior\n4. Agrupar los patches de DOM resultantes\n\nEl punto crítico es el paso 1. Cuando un pedazo de estado cambia en un componente padre, React re-ejecuta *todos los componentes hijos* en ese subárbol por defecto. El algoritmo de diff es rápido, sí, pero se está diffeando árboles que son 95% idénticos porque solo una hoja cambió.\n\n```javascript\n// Este componente se re-renderiza cuando CUALQUIER parte\n// de appState cambia\nfunction Dashboard({ appState }) {\n return (\n <div>\n <Header user={appState.user} />\n <Sidebar notifications={appState.notifications} />\n <MainContent data={appState.data} />\n {/* Si appState.notifications cambia, MainContent\n igual se re-ejecuta. El diff capta que su output\n no cambió, pero la función ya corrió. */}\n </div>\n );\n}\n```\n\nLa respuesta de React fue ` React.memo `, ` useMemo `, ` useCallback `, hints de memoización manual que los desarrolladores consistentemente la embarran. Revisé codebases de producción donde usaban ` useMemo ` en valores primitivos (innecesariamente) y lo omitían en computaciones caras (anulando el propósito)."},{"heading":"Entran los Signals: Reactividad Quirúrgica","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#entran-los-signals-reactividad-quirrgica","content":"Los signals invierten el modelo. En vez de re-rendering top-down con diffing bottom-up, los signals trackean dependencias al momento de lectura y actualizan únicamente los nodos DOM específicos que dependen de los valores que cambiaron.\n\nA continuación se muestra lo que [SolidJS](<https://www.solidjs.com/>) hace internamente cuando escribirs:\n\n```javascript\nimport { createSignal, createEffect } from \"solid-js\";\n\nconst [count, setCount] = createSignal(0);\nconst doubled = () => count() * 2;\n\ncreateEffect(() => {\n // Solid trackea que este effect lee `count`\n // Cuando count cambia, SOLO este effect se re-ejecuta\n console.log(\"Count es:\", count());\n});\n\n// En un componente:\nfunction Counter() {\n const [count, setCount] = createSignal(0);\n\n // Este JSX se compila a operaciones DOM directas.\n // La expresión {count()} se convierte en una\n // suscripción fine-grained que actualiza UN text node.\n return (\n <div>\n <span>{count()}</span>\n <button onClick={() => setCount(c => c + 1)}>+</button>\n </div>\n );\n}\n```\n\nLa salida de compilación se ve más o menos así (simplificado):\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 // Suscripción directa: 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\nSin virtual DOM. Sin diffing. Sin re-ejecución de componentes. El signal escribe directo al text node. Perfilé esto en un dashboard real que mantengo, un panel de monitoreo con ~400 valores actualizándose, y la diferencia fue notable. React con memoización correcta: ~12ms por ciclo de actualización. SolidJS: ~0.8ms. Esto no es un juego de benchmarks; es una app de producción renderizando datos reales de WebSocket."},{"heading":"Angular Signals: La Revolución Incremental","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#angular-signals-la-revolucin-incremental","content":"El enfoque de Angular es interesante porque no podían volar por el aire su modelo existente. Tenían que hacer signals opt-in e interoperable con la detección de cambios basada en zone.js.\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>Valor total: {{ 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 console.log(`Mostrando ${this.filteredItems().length} items`);\n });\n }\n}\n```\n\nLo que me parece notable de la implementación de Angular es el primitivo ` computed() `. Es lazy, no recalcula hasta que lo leés, y cachea su valor. Si ` filter ` cambia pero produce el mismo resultado de ` filteredItems `, ` totalValue ` ni se inmuta. Esa garantía libre de glitches importa en grafos de dependencias complejos.\n\nLa implementación interna usa un counter de versión en cada nodo signal. Cuando llamás ` signal.set() `, incrementa la versión. Los signals computed chequean las versiones upstream antes de recalcular, si nada cambió, devuelven el valor cacheado sin recorrer el grafo."},{"heading":"Preact Signals: El Caballo de Troya","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#preact-signals-el-caballo-de-troya","content":"Preact Signals hizo algo audaz: hicieron que signals funcionen dentro de React. El paquete ` @preact/signals-react ` intercepta el rendering de componentes y reemplaza las lecturas de signals con suscripciones fine-grained.\n\n```javascript\nimport { signal, computed } from \"@preact/signals-react\";\n\n// Estos viven FUERA de los componentes, son estado a nivel módulo\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// Este componente se re-renderiza SOLO cuando filteredTodos cambia\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\nUsé esto en un proyecto personal el año pasado, un dashboard de trading en tiempo real, y los resultados fueron impresionantes. Pero hay filos cortantes. El adapter de React funciona wrapeando tus componentes en un HOC que configura tracking de signals. Esto interactúa mal con:\n\n- React DevTools (los nombres de componentes quedan mangled)\n- Las optimizaciones del compiler de React 19 (el compiler asume semántica React estándar)\n- Features concurrentes como ` useTransition ` (signals son sincrónicos; transitions no)\n\nMi opinión: Preact Signals en Preact es excelente. Preact Signals en React es un hack ingenioso que se vuelve cada vez más frágil a medida que React evoluciona."},{"heading":"La Propuesta TC39: Estandarizando el Primitivo","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#la-propuesta-tc39-estandarizando-el-primitivo","content":"La propuesta TC39 de Signals (actualmente Stage 1, liderada por Daniel Ehrenberg y otros) busca estandarizar un primitivo signal mínimo en JavaScript mismo. Así se ve la API propuesta:\n\n```javascript\n// API estándar propuesta\nconst counter = new Signal.State(0);\nconst doubled = new Signal.Computed(() => counter.get() * 2);\n\n// Lectura\ncounter.get(); // 0\ndoubled.get(); // 0\n\n// Escritura\ncounter.set(5);\ndoubled.get(); // 10\n\n// La API Watcher para scheduling\nconst watcher = new Signal.subtle.Watcher(() => {\n // Se llama sincrónicamente cuando un signal observado\n // se marca como dirty\n // Los frameworks usan esto para agendar su propio\n // pase de rendering\n queueMicrotask(processPendingUpdates);\n});\nwatcher.watch(doubled);\n```\n\nEl insight clave: la propuesta deliberadamente excluye ` effect() `. Los effects, reacciones con side-effects a cambios de signals, son donde los frameworks más divergen. Los effects de SolidJS son sincrónicos. Los de Angular son agendados. El ` watchEffect ` de Vue batchea en la cola de microtasks. La propuesta TC39 provee ` Signal.subtle.Watcher ` como hook de bajo nivel, dejando que cada framework implemente su propio scheduling de effects.\n\nLeí la implementación del polyfill de la propuesta, y el tracking de dependencias usa una lista doblemente enlazada entre nodos signal. Cuando un ` Computed ` evalúa, captura lecturas vía un contexto de tracking global (similar a como funcionan internamente SolidJS y Vue):\n\n```javascript\n// Tracking de signals simplificado (conceptual)\nlet currentComputed = null;\n\nclass Signal {\n #value;\n #dependents = new Set();\n\n get() {\n // Trackear la dependencia\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 // Notificar dependientes\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/es/blog/signals-reactivity-angular-solid-preact#benchmarks-fine-grained-vs-virtual-dom","content":"Corrí la suite JS Framework Benchmark en mi workstation (Ryzen 9 7950X, 64GB DDR5, Chrome 123) entre frameworks. Acá van los resultados que importan, los que reflejan patrones reales de apps, no micro-benchmarks:\n\n**Actualización parcial (actualizando cada 10ma fila en una tabla de 10.000 filas):**\n\n| Framework | Tiempo (ms) | Relativo |\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**Intercambio de filas (intercambiar 2 filas en una tabla de 1.000 filas):**\n\n| Framework | Tiempo (ms) | Relativo |\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\nEl patrón es consistente: los frameworks con reactividad fine-grained (Solid, runes de Svelte 5, Preact+Signals) superan a los frameworks de virtual DOM en actualizaciones parciales. La brecha se amplía con árboles de componentes más grandes porque el enfoque vDOM escala con el tamaño del árbol, mientras que signals escala con el tamaño del cambio."},{"heading":"Donde los Signals se Quedan Cortos","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#donde-los-signals-se-quedan-cortos","content":"Sería deshonesto si no mencionara los tradeoffs.\n\n**Debuggear es más difícil.** En React, es posible meter un console.log arriba del componente y ver cada re-render. Con signals, las actualizaciones pasan a nivel de binding, no hay un momento \"esto se renderizó\" a nivel componente. Las DevTools de SolidJS han mejorado, pero aún no alcanzan el nivel de madurez de las React DevTools.\n\n**Diamantes de estado derivado.** Considerá:\n\n```javascript\nconst a = signal(1);\nconst b = computed(() => a.value + 1); // depende de a\nconst c = computed(() => a.value + 2); // depende de a\nconst d = computed(() => b.value + c.value); // depende de b y c\n```\n\nCuando ` a ` cambia, tanto ` b ` como ` c ` se marcan dirty. Si ` d ` evalúa eagerly cuando ` b ` se actualiza pero antes de que ` c ` se actualice, teners un \"glitch\", un estado transitorio inconsistente. Todas las implementaciones serias de signals manejan esto (SolidJS usa ordenamiento topológico, Angular usa un algoritmo push-pull), pero es una clase de bug que no existe en el modelo \"re-renderizar todo\".\n\n**Server-side rendering.** Los signals son inherentemente primitivos del cliente. SSR requiere serializar valores de signals e hidratarlos en el cliente, cosa que cada framework maneja distinto. El approach de SolidJS (su ` renderToString ` captura valores iniciales) funciona bien, pero es más complejo que el modelo \"renderizar el componente a HTML\" de React."},{"heading":"Mi Opinión: La Industria Está Convergiendo","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#mi-opinin-la-industria-est-convergiendo","content":"Esto es lo que creo que está pasando realmente. No estamos en una \"guerra signals vs. virtual DOM\". Estamos viendo a la industria converger en reactividad compilada:\n\n- **El compiler de React 19** analiza tu código en build time e inserta memoización automáticamente\n- **Las runes de Svelte 5** compilan sintaxis tipo signal en operaciones DOM fine-grained\n- **SolidJS** siempre compiló JSX en bindings DOM directos\n- **Vue Vapor** (en desarrollo) compila templates en operaciones basadas en signals sin vDOM\n\nEl estado final es el mismo: el framework deduce el set mínimo de mutaciones DOM en compile time o a través de tracking en runtime, y los desarrolladores escriben código declarativo directo. Los signals están ganando porque son el primitivo runtime más natural para esto, pero el compiler es igual de importante.\n\nSi se está arrancando un proyecto nuevo hoy, mi recomendación honesta: SolidJS para máxima performance y bundle chico, Angular con signals para equipos enterprise que necesitan la experiencia full-framework, Svelte 5 para la mejor developer experience. React sigue siendo una opción perfectamente válida, el ecosistema no tiene rival, pero sabé que se está pagando un impuesto runtime que otros frameworks ya eliminaron.\n\nLa propuesta TC39 no va a salir por lo menos en dos años más. Mientras tanto, el patrón es claro: reactividad fine-grained, ya sea a través de signals o magia del compiler, es el futuro. El virtual DOM fue una innovación brillante para su época. Su época está pasando."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#questions","content":""},{"heading":"¿Los signals son observables con otro nombre?","level":3,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#question-1","content":"No. Los observables son streams push-based que requieren manejo manual de suscripciones. Los signals son primitivos reactivos sincrónicos con tracking automático de dependencias, el runtime arma un DAG en el momento de lectura, no de suscripción. Sin teardown, sin memory leaks por unsubscribes olvidados."},{"heading":"¿La propuesta TC39 de Signals va a reemplazar las implementaciones de cada framework?","level":3,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#question-2","content":"Eventualmente, parcialmente. La propuesta TC39 (Stage 1 a principios de 2026) define un primitivo Signal mínimo. Los frameworks van a seguir armando su propio scheduling, batching e integración de rendering encima. Pensalo como un cimiento compartido, no un reemplazo."},{"heading":"¿Debería reescribir mi app React para usar signals?","level":3,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#question-3","content":"Probablemente no. El compiler de React (React Forget / React 19) resuelve el problema de re-renders de otra manera, mediante memoización automática. Signals y reactividad compilada están convergiendo al mismo objetivo desde ángulos distintos. El costo de migración rara vez justifica las mejoras de performance salvo que tengas cuellos de botella medibles."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#article-tips","content":"1. **Tip:** Al migrar componentes Angular a signals, comenzar con ` computed() ` para estado derivado antes de utilizar ` effect() `, los effects son el punto de riesgo, no los signals en sí.\n2. **Tip:** El adapter ` @preact/signals-react ` funciona en React, pero con precaución, aplica monkey-patch a ` React.createElement `, lo que genera conflictos con las optimizaciones del compiler de React 19.\n3. **Tip:** En SolidJS, nunca desestructurar props en los argumentos del componente. ` const MyComp = (props) => <div>{props.name}</div> ` es reactivo; ` const MyComp = ({name}) => <div>{name}</div> ` elimina la reactividad por completo.\n\n- signals\n- reactividad\n- angular\n- solidjs\n- preact\n- tc39\n- virtual-dom\n- reactividad-fine-grained\n- arquitectura-frontend\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact"},{"text":"SolidJS","url":"https://www.solidjs.com/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact"},"headline":"Signals y Reactividad Fine-Grained: Angular, SolidJS, Preact y la Transición del Diffing de Virtual DOM","description":"La reactividad fine-grained se está imponiendo. Análisis del patrón de signals desde SolidJS pasando por Angular 17+ y Preact Signals, la propuesta TC39, y benchmarks que muestran por qué el diffing de virtual DOM se está convirtiendo en un costo innecesario.","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/es/blog/signals-reactivity-angular-solid-preact","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Frontend","keywords":"signals, reactividad, angular, solidjs, preact, tc39, virtual-dom, reactividad-fine-grained, arquitectura-frontend","wordCount":1856,"timeRequired":"PT10M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/signals-reactivity-angular-solid-preact"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"Signals y Reactividad Fine-Grained: Angular, SolidJS, Preact y la Transición del Diffing de Virtual DOM","item":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Los signals son observables con otro nombre?","acceptedAnswer":{"@type":"Answer","text":"No. Los observables son streams push-based que requieren manejo manual de suscripciones. Los signals son primitivos reactivos sincrónicos con tracking automático de dependencias, el runtime arma un DAG en el momento de lectura, no de suscripción. Sin teardown, sin memory leaks por unsubscribes olvidados."}},{"@type":"Question","name":"¿La propuesta TC39 de Signals va a reemplazar las implementaciones de cada framework?","acceptedAnswer":{"@type":"Answer","text":"Eventualmente, parcialmente. La propuesta TC39 (Stage 1 a principios de 2026) define un primitivo Signal mínimo. Los frameworks van a seguir armando su propio scheduling, batching e integración de rendering encima. Pensalo como un cimiento compartido, no un reemplazo."}},{"@type":"Question","name":"¿Debería reescribir mi app React para usar signals?","acceptedAnswer":{"@type":"Answer","text":"Probablemente no. El compiler de React (React Forget / React 19) resuelve el problema de re-renders de otra manera, mediante memoización automática. Signals y reactividad compilada están convergiendo al mismo objetivo desde ángulos distintos. El costo de migración rara vez justifica las mejoras de performance salvo que tengas cuellos de botella medibles."}}]}]}],"contentHash":"7c9384c38656c3bbf2f2c8a6ec8ed2083214f96a8691558d7700d934571792c6","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":73101,"contentBytes":15535}}
