wandres.dev
COMPILAR LA REACTIVIDAD · Cuando el compilador hace el trabajo

Lo que se pierde: depurabilidad y portabilidad

El coste de compilar la reactividad — trazas que señalan código generado, comportamiento que depende de la versión del compilador, y lógica que no sale de su ecosistema — y cómo mitigarlo.

⏱ 16 min

Cerramos el nivel con la cara que las presentaciones de compiladores nunca enseñan. Cuando el código que se ejecuta no es el que escribiste, todo lo que depende de esa correspondencia se degrada: las trazas de pila, los puntos de interrupción, la lectura del código por un compañero, la portabilidad de la lógica. Ninguno de esos costes anula las ventajas, y todos merecen contabilizarse antes de decidir.

🎯 Al terminar esta lección sabrás
  • Enumerar los cuatro costes de la reactividad compilada.
  • Evaluar hasta dónde los mitigan los mapas de fuentes.
  • Reconocer el acoplamiento a la versión del compilador.
  • Decidir con criterio cuánta magia aceptar.

Coste uno: las trazas señalan otro código

Cuando algo falla dentro del runtime, la pila incluye funciones que no aparecen en tu fichero. Los mapas de fuentes traducen las posiciones y ayudan mucho, con dos límites concretos.

Los nombres no se traducen. La pila muestra los nombres de las funciones generadas, y aunque la línea apunte a tu código, la relación entre lo que ves y lo que se ejecutó no es evidente.

El código intermedio no existe en tu fuente. Si el fallo ocurre en una llamada que el compilador insertó, no hay línea de tu fichero que le corresponda de verdad, y el mapa apunta a la más cercana, que puede confundir más que ayudar.

La mitigación real es que los motores compilados invierten mucho en mensajes de error. Cuando la advertencia dice exactamente qué regla se ha incumplido y en qué construcción, la traza importa menos. Es una compensación deliberada y funciona bastante bien en la práctica.

Coste dos: los puntos de interrupción se comportan raro

Poner un punto de interrupción en una línea que el compilador ha transformado puede no detener donde esperas, o detener varias veces, o no detener nunca si la línea se eliminó por ser estática.

El caso más desconcertante es el de una expresión que el compilador ha extraído a un efecto: al detenerte ahí, la pila muestra el vaciado de la cola en vez del código que provocó la actualización, porque la ejecución ocurre en un momento distinto al de la línea donde está escrita.

La técnica que funciona es depurar por el estado y no por el flujo: en vez de seguir la ejecución paso a paso, instrumentar el grafo —qué nodos se marcaron, cuáles se ejecutaron, con qué valores— y razonar sobre esos datos. Es la conclusión del nivel 2 sobre volcar el grafo antes de perfilar, aplicada a la depuración.

💡
Un registro de nodos vale mas que un punto de interrupcion

En un sistema reactivo compilado, la herramienta más útil no es el depurador sino un registro que anote, por cada ciclo, qué se escribió, qué nodos se marcaron y cuáles se ejecutaron. Con esa traza se diagnostica en minutos lo que con puntos de interrupción cuesta una tarde, porque el problema casi siempre es este nodo se ejecutó cuando no debía o este nodo no se ejecutó cuando debía, y las dos preguntas se responden mirando el registro.

Coste tres: el acoplamiento a la versión

El código generado es un detalle de implementación que cambia entre versiones del compilador. Eso tiene tres consecuencias.

El comportamiento en los bordes puede cambiar. Casos que dependen del orden exacto o de si una expresión se envolvió en un efecto pueden comportarse distinto tras una actualización. No es frecuente, y cuando pasa es difícil de diagnosticar porque tu código no ha cambiado.

Las herramientas tienen que ir sincronizadas. El plugin de lint, la extensión del editor, el formateador y el propio compilador tienen que entenderse entre sí. Una desincronización produce falsos avisos o transformaciones incorrectas.

Depurar el compilador no es opción. Si sospechas que la transformación es incorrecta, tienes que leer la salida generada y compararla con lo que esperabas. Es un nivel de abstracción más del que tienes con un motor sin compilador.

La portabilidad y cuánta magia aceptar

Coste cuatro: la portabilidad

La lógica reactiva escrita con sintaxis compilada no sale de su ecosistema. Un cálculo derivado expresado con runes no se puede usar desde código que no pase por el compilador de Svelte. Una plantilla JSX de grano fino no se puede reutilizar con otro motor.

Compáralo con un motor sin compilador: una función que crea señales y memos es JavaScript normal, y se puede usar en un script, en un test, en un worker o en otro framework que use el mismo motor. La portabilidad de la lógica de negocio no es un asunto menor en proyectos de vida larga.

Esto es exactamente lo que la propuesta de Signals de TC39 pretende resolver por debajo: si el primitivo está en el lenguaje, la lógica escrita sobre él es portable con independencia del framework que la consuma, y los compiladores pueden seguir haciendo su trabajo encima.

flowchart TB
C[compilar la reactividad] --> G[ganancias]
C --> P[perdidas]
G --> G1[sintaxis limpia]
G --> G2[plantillas clonables]
G --> G3[nada reactivo para lo estatico]
G --> G4[memoizacion sin escribirla]
P --> P1[trazas sobre codigo generado]
P --> P2[puntos de interrupcion imprecisos]
P --> P3[acoplamiento a la version]
P --> P4[logica no portable]
style C fill:#fab387,color:#11111b
style G fill:#a6e3a1,color:#11111b
style P fill:#f9e2af,color:#11111b
style G1 fill:#a6e3a1,color:#11111b
style G2 fill:#a6e3a1,color:#11111b
style G3 fill:#a6e3a1,color:#11111b
style G4 fill:#a6e3a1,color:#11111b
style P1 fill:#f9e2af,color:#11111b
style P2 fill:#f9e2af,color:#11111b
style P3 fill:#f9e2af,color:#11111b
style P4 fill:#f9e2af,color:#11111b

Cuánta magia aceptar

La decisión se puede plantear con una pregunta que separa bien los casos: ¿qué pasa cuando el modelo falla?

Si el equipo entiende el motor por debajo, un compilador es una comodidad neta: cuando algo raro ocurre, se lee la salida generada, se reconoce el mecanismo y se arregla. La magia se desmonta cuando hace falta.

Si el equipo solo conoce la sintaxis, un compilador es un riesgo: cuando algo raro ocurre no hay dónde mirar, porque el modelo que se conoce no explica el comportamiento observado. Y el modo de fallo típico de los motores de grano fino —silencio, algo que deja de actualizarse— es especialmente malo en esa situación.

Dicho de otro modo, la magia es aceptable en proporción a lo bien que se conoce lo que hay debajo, que es precisamente el argumento a favor de haber leído los once niveles anteriores antes de este.

Compilar la reactividad es una optimizacion, no una arquitectura

La conclusión del nivel, y quizá la más importante para leer con criterio lo que se publique en los próximos años: ninguno de los tres compiladores que hemos visto cambia el modelo de reactividad. Las runes generan las mismas llamadas al mismo grafo. El compilador de JSX de Solid genera los mismos efectos sobre los mismos nodos. React Compiler genera las mismas memoizaciones que se escribían a mano. En los tres casos, lo que hay debajo es exactamente la máquina de los once niveles anteriores, y por eso todo lo que has aprendido sigue aplicándose línea a línea. Un compilador de reactividad es una optimización de la superficie: quita ruido sintáctico, precalcula lo que no depende del estado y genera código que un humano escribiría igual pero peor y más despacio. Eso es enormemente valioso y no es una arquitectura nueva. La consecuencia práctica es concreta y sirve como criterio de evaluación permanente: cuando aparezca un framework anunciando que su compilador hace la reactividad desaparecer, la pregunta que hay que hacerle es qué grafo construye en tiempo de ejecución y quién lo recorre. Si la respuesta es que ninguno, o ha adoptado la aproximación conservadora —comprobable en diez líneas con una guarda condicional— o está reconciliando. Si la respuesta es un grafo, entonces es la misma máquina con otra ropa, y ya sabes cómo funciona por dentro.

⚔️ Desmonta la magia
  1. Coge un componente reactivo compilado y examina la salida generada.
  2. Localiza cada una de las cinco piezas del nivel 0 en ese código generado.
  3. Provoca un fallo de reactividad y compara lo que dice la traza con lo que dice un registro de nodos marcados y ejecutados.
  4. Escribe la misma lógica sin compilador, con llamadas explícitas, y compara la legibilidad de ambos cuando algo falla.