Preact Signals y la tabla comparada
El motor más pequeño de la familia, con listas enlazadas y versiones por arista, y la tabla que sitúa a los cinco en las ocho dimensiones de diseño con lo que paga cada elección.
Preact Signals es el más pequeño de los cinco y el que más deliberadamente limita su alcance: no tiene árbol de dueños, no tiene estado profundo y no tiene planificador propio. Esas ausencias son decisiones, no carencias, y entenderlas es la mejor manera de cerrar la comparación. Al final de la lección, la tabla que sitúa a los cinco motores en las ocho dimensiones que hemos ido construyendo.
- Situar las decisiones de Preact Signals y sus ausencias deliberadas.
- Entender su representación con listas enlazadas y versiones por arista.
- Leer la tabla comparada de los cinco motores.
- Extraer el criterio de elección que no depende del rendimiento.
Preact Signals
Representación. Listas doblemente enlazadas donde cada arista es un objeto con punteros a los dos lados y un número de versión. Es el diseño que estudiamos en el nivel 2 y el que permite reutilizar aristas sin asignar nada cuando las dependencias no cambian.
Descubrimiento. Tracking automático con un contexto de evaluación. untracked para leer sin rastrear y .peek() para leer una señal concreta sin suscribirse.
Propagación. Push-pull con banderas de bits por nodo y comprobación de versiones. La escritura notifica hacia adelante y la lectura verifica hacia atrás.
Consistencia. Glitch-free por el mismo mecanismo de dos fases.
Memoización. computed, perezoso y cacheado.
Ciclo de vida. No hay árbol de dueños. effect devuelve una función de baja y el programador —o el framework que lo integra— decide cuándo llamarla.
Planificación. Síncrona, con batch explícito.
Modelo de estado. Solo atómico, con acceso por .value.
import { signal, computed, effect, batch } from '@preact/signals-core';
const contador = signal(0);
const doble = computed(() => contador.value * 2);
const tirar = effect(() => console.log(doble.value));
batch(() => { contador.value = 1; contador.value = 2; });
tirar(); // baja manual
contador.peek(); // leer sin suscribirse
Por qué las ausencias son decisiones
Preact Signals se diseñó como el núcleo reactivo mínimo, pensado para integrarse en un framework que ya resuelve lo demás. Los paquetes de integración con Preact y con React se encargan de atarlo al ciclo de vida de los componentes y al ciclo de renderizado.
Visto así, no tener árbol de dueños es coherente: el framework anfitrión ya sabe cuándo se desmonta un componente. No tener estado profundo también: se puede construir encima si hace falta, y no hacerlo mantiene el paquete pequeño. Y no tener planificador propio significa que se alinea con el del anfitrión en vez de competir con él.
El resultado es un motor de unos pocos kilobytes que hace una cosa y la hace completa. Para incrustar reactividad en un widget, en una extensión o en un sistema que ya tiene su propia arquitectura, esa austeridad es exactamente lo que se quiere.
Por su tamaño y por su ausencia de dependencias, es el más fácil de leer entero en una tarde y el más fácil de usar en un banco de pruebas para comparar modelos de propagación. Si vas a implementar el motor del nivel 12 y quieres contrastar tu resultado con uno real, este es el candidato natural.
La tabla
| dimensión | Solid | Vue | Angular | Svelte 5 | Preact Signals |
|---|---|---|---|---|---|
| aristas | arrays con índices cruzados | lista enlazada, reutilización | listas con versiones | listas con banderas | lista enlazada, versión por arista |
| descubrimiento | automático, untrack, on |
automático, proxy o .value |
automático, untracked |
automático, insertado por el compilador | automático, untracked, .peek() |
| propagación | dos fases, STALE y PENDING |
dos fases con niveles y versión | versiones más época global | dos fases con banderas | dos fases con banderas y versión |
| memoización | createMemo con equals |
computed, customRef |
computed con equal |
$derived |
computed |
| ciclo de vida | árbol de dueños completo | effectScope |
el inyector | $effect.root |
ninguno, baja manual |
| planificación | síncrona, batch |
asíncrona, flush con tres modos |
asíncrona, planificador propio | por tick, $effect.pre |
síncrona, batch |
| estado | atómico y almacén profundo | ref y reactive |
solo atómico | $state unificado |
solo atómico |
| sintaxis | llamada de función | .value o propiedad |
llamada de función | variable normal | .value |
Qué paga cada decisión y cómo elegir
Qué paga cada decisión
Arrays con índices cruzados pagan complejidad de implementación y ganan localidad de caché. Listas enlazadas con reutilización pagan memoria por arista y ganan cero asignaciones cuando las dependencias no cambian.
Ejecución síncrona paga tener que agrupar a mano y ganar trazas completas. Asíncrona paga una ventana de inconsistencia y contagio de asincronía, y gana agrupamiento automático y alineación con el pintado.
Árbol de dueños completo paga memoria por nodo y una API más grande, y gana que crear efectos no requiera pensar en el ciclo de vida. Sin árbol paga responsabilidad del programador y gana tamaño.
Estado profundo paga indirección en cada acceso y la invisibilidad de la reactividad, y gana granularidad automática. Solo atómico paga que el grano lo decidas tú y gana lecturas baratas y explicitud.
Sintaxis compilada paga depurabilidad y portabilidad, y gana la ergonomía más limpia.
flowchart TB E[eje de la explicitud] --> S[Solid llamada visible] E --> P[Preact punto value] E --> V[Vue mixto] E --> A[Angular llamada visible] E --> W[Svelte invisible] S --> R1[mas facil de razonar] W --> R2[mas facil de escribir] style E fill:#f9e2af,color:#11111b style S fill:#89b4fa,color:#11111b style P fill:#89b4fa,color:#11111b style V fill:#cba6f7,color:#11111b style A fill:#89b4fa,color:#11111b style W fill:#a6e3a1,color:#11111b style R1 fill:#94e2d5,color:#11111b style R2 fill:#94e2d5,color:#11111b
El criterio que no depende del rendimiento
Después de cincuenta lecciones de mecánica, la conclusión práctica sobre cuál elegir no es de rendimiento, y conviene decirlo sin rodeos: los cinco implementan el mismo algoritmo con las mismas garantías, y sus diferencias de velocidad, aunque medibles, están dominadas en cualquier aplicación real por la forma del grafo que escriba el programador, que es una variable con muchísimo más rango.
Lo que sí difiere de verdad es lo demás. La superficie de API y cuánto hay que saber para usarla bien. Los modos de fallo característicos y si son ruidosos o silenciosos. Lo fácil que resulta razonar sobre el código escrito seis meses antes. Lo que ya sabe tu equipo. El tamaño del ecosistema para lo que necesitas. Y si necesitas un motor solo o un framework entero.
Vale la pena detenerse en lo notable de esta tabla: cinco equipos independientes, partiendo de puntos muy distintos y con restricciones incompatibles, acabaron con la misma arquitectura. Los cinco usan tracking automático con una variable de módulo. Los cinco usan push-pull con marcado en dos fases. Los cinco usan comparación de igualdad para cortar la propagación. Los cinco resolvieron el rombo igual. Las diferencias están todas en la periferia —la sintaxis, la planificación, el ciclo de vida, la representación de las aristas— y ninguna toca el núcleo. Cuando ves algo así en ingeniería, la interpretación correcta no es que se copiaron: es que el espacio de soluciones buenas es muy pequeño, y el problema tiene una estructura que empuja a todo el mundo al mismo sitio. Ese es el mejor argumento posible a favor de estandarizar el primitivo, y es exactamente el razonamiento de la propuesta de Signals en TC39, cuyo grupo incluye a autores de todos estos motores. Si la parte que comparten es la difícil, la que ya no cambia y la que impide la interoperabilidad por ser una variable de módulo, tiene todo el sentido moverla al lenguaje y dejar que cada framework se quede con lo que de verdad lo diferencia, que es la periferia. Para ti tiene una consecuencia inmediata y tranquilizadora: lo que has aprendido en este track no caduca cuando cambies de framework, porque lo que has aprendido es justo la parte que los cinco comparten.
- Implementa el mismo grafo de prueba en los cinco motores, con contadores de ejecuciones de cuerpo.
- Ejecuta el escenario de la cadena con corte del nivel 4 y compara los números.
- Ejecuta el escenario donde todo cambia y comprueba si el orden se mantiene.
- Anota cuántas líneas te costó expresar el mismo grafo en cada uno, y compara esa cifra con las diferencias de rendimiento.