<!-- Canonical: https://lucioduran.com/es/blog/signals-reactivity-angular-solid-preact
Language: es
Content-SHA256: 7c9384c38656c3bbf2f2c8a6ec8ed2083214f96a8691558d7700d934571792c6
-->

Frontend

# Signals y Reactividad Fine-Grained: Angular, SolidJS, Preact y la Transición del Diffing de Virtual DOM

2026-02-0110 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/signals-reactivity-angular-solid-preact>)

## El Costo del Diffing de Virtual DOM

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:

1. Re-ejecutar toda tu función de componente en cada cambio de estado
2. Producir un árbol virtual DOM nuevo
3. Correr un diff recursivo O(n) contra el árbol anterior
4. Agrupar los patches de DOM resultantes

El 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ó.

```javascript
// Este componente se re-renderiza cuando CUALQUIER parte
// de appState cambia
function Dashboard({ appState }) {
 return (
 <div>
 <Header user={appState.user} />
 <Sidebar notifications={appState.notifications} />
 <MainContent data={appState.data} />
 {/* Si appState.notifications cambia, MainContent
 igual se re-ejecuta. El diff capta que su output
 no cambió, pero la función ya corrió. */}
 </div>
 );
}
```

La 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).

## Entran los Signals: Reactividad Quirúrgica

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.

A continuación se muestra lo que [SolidJS](<https://www.solidjs.com/>) hace internamente cuando escribirs:

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

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

createEffect(() => {
 // Solid trackea que este effect lee `count`
 // Cuando count cambia, SOLO este effect se re-ejecuta
 console.log("Count es:", count());
});

// En un componente:
function Counter() {
 const [count, setCount] = createSignal(0);

 // Este JSX se compila a operaciones DOM directas.
 // La expresión {count()} se convierte en una
 // suscripción fine-grained que actualiza UN text node.
 return (
 <div>
 <span>{count()}</span>
 <button onClick={() => setCount(c => c + 1)}>+</button>
 </div>
 );
}
```

La salida de compilación se ve más o menos así (simplificado):

```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");

 // Suscripción directa: 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;
}
```

Sin 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.

## Angular Signals: La Revolución Incremental

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.

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

@Component({
 selector: 'app-inventory',
 template: `
 <div>
 <p>Items: {{ items().length }}</p>
 <p>Valor total: {{ 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(() => {
 console.log(`Mostrando ${this.filteredItems().length} items`);
 });
 }
}
```

Lo 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.

La 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.

## Preact Signals: El Caballo de Troya

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.

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

// Estos viven FUERA de los componentes, son estado a nivel módulo
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);
});

// Este componente se re-renderiza SOLO cuando filteredTodos cambia
function TodoList() {
 return (
 <ul>
 {filteredTodos.value.map(todo => (
 <TodoItem key={todo.id} todo={todo} />
 ))}
 </ul>
 );
}
```

Usé 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:

- React DevTools (los nombres de componentes quedan mangled)
- Las optimizaciones del compiler de React 19 (el compiler asume semántica React estándar)
- Features concurrentes como ` useTransition ` (signals son sincrónicos; transitions no)

Mi 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.

## La Propuesta TC39: Estandarizando el Primitivo

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:

```javascript
// API estándar propuesta
const counter = new Signal.State(0);
const doubled = new Signal.Computed(() => counter.get() * 2);

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

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

// La API Watcher para scheduling
const watcher = new Signal.subtle.Watcher(() => {
 // Se llama sincrónicamente cuando un signal observado
 // se marca como dirty
 // Los frameworks usan esto para agendar su propio
 // pase de rendering
 queueMicrotask(processPendingUpdates);
});
watcher.watch(doubled);
```

El 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.

Leí 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):

```javascript
// Tracking de signals simplificado (conceptual)
let currentComputed = null;

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

 get() {
 // Trackear la dependencia
 if (currentComputed) {
 this.#dependents.add(currentComputed);
 currentComputed.addSource(this);
 }
 return this.#value;
 }

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

## Benchmarks: Fine-Grained vs. Virtual DOM

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:

**Actualización parcial (actualizando cada 10ma fila en una tabla de 10.000 filas):**

| Framework | Tiempo (ms) | Relativo |
| --- | --- | --- |
| 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 |

**Intercambio de filas (intercambiar 2 filas en una tabla de 1.000 filas):**

| Framework | Tiempo (ms) | Relativo |
| --- | --- | --- |
| 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 |

El 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.

## Donde los Signals se Quedan Cortos

Sería deshonesto si no mencionara los tradeoffs.

**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.

**Diamantes de estado derivado.** Considerá:

```javascript
const a = signal(1);
const b = computed(() => a.value + 1); // depende de a
const c = computed(() => a.value + 2); // depende de a
const d = computed(() => b.value + c.value); // depende de b y c
```

Cuando ` 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".

**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.

## Mi Opinión: La Industria Está Convergiendo

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:

- **El compiler de React 19** analiza tu código en build time e inserta memoización automáticamente
- **Las runes de Svelte 5** compilan sintaxis tipo signal en operaciones DOM fine-grained
- **SolidJS** siempre compiló JSX en bindings DOM directos
- **Vue Vapor** (en desarrollo) compila templates en operaciones basadas en signals sin vDOM

El 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.

Si 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.

La 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.

## Preguntas frecuentes

### ¿Los signals son observables con otro nombre?

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.

### ¿La propuesta TC39 de Signals va a reemplazar las implementaciones de cada framework?

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.

### ¿Debería reescribir mi app React para usar signals?

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.

## Tips

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í.
2. **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.
3. **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.

- signals
- reactividad
- angular
- solidjs
- preact
- tc39
- virtual-dom
- reactividad-fine-grained
- arquitectura-frontend

[Volver al Blog](<https://lucioduran.com/es/blog>)
