Una condicional cambia el grafo en cada ejecución
Las dependencias condicionales son la propiedad que distingue al tracking automático, y también la fuente de sus comportamientos más desconcertantes: aristas que aparecen y desaparecen, orden de lectura que importa y cortocircuitos que cortan dependencias.
Un if en el cuerpo de una computación no es solo control de flujo: es una reconfiguración del grafo. Cada rama produce un conjunto distinto de aristas, y como la rama depende de valores que cambian, el grafo cambia con ellos. Esta lección explora las consecuencias, que van desde una optimización valiosísima hasta tres trampas que todo el mundo pisa una vez.
- Trazar cómo se reconfigura el grafo al cambiar de rama.
- Aprovechar las dependencias condicionales como técnica de optimización.
- Reconocer el efecto de los operadores de cortocircuito sobre las aristas.
- Detectar los tres patrones que producen dependencias inestables.
La reconfiguración
Sigamos paso a paso lo que ocurre cuando una condicional cambia de rama.
const modo = senal('lista');
const elementos = senal([1, 2, 3]);
const detalle = senal({ id: 7 });
const vista = memo(() => modo() === 'lista' ? elementos().length : detalle().id);
Con modo igual a 'lista', el grafo tiene dos aristas: modo a vista, elementos a vista. Escribir en detalle no hace absolutamente nada.
Ahora modo.set('detalle'). La secuencia completa es esta. La escritura marca vista como sucio. Alguien lee vista, que se reevalúa: primero desata sus dos aristas, dejando elementos sin observadores; luego ejecuta, leyendo modo —arista nueva— y detalle —arista nueva. Resultado: dos aristas, una de ellas distinta.
A partir de ese momento, escribir en elementos no produce nada. La lista puede cambiar mil veces mientras estemos en modo detalle y el motor no gastará un ciclo.
flowchart TB subgraph antes M1[modo lista] --> V1[vista] E1[elementos] --> V1 D1[detalle] -.sin arista.-> V1 end subgraph despues M2[modo detalle] --> V2[vista] E2[elementos] -.sin arista.-> V2 D2[detalle] --> V2 end style M1 fill:#89b4fa,color:#11111b style E1 fill:#89b4fa,color:#11111b style D1 fill:#f9e2af,color:#11111b style V1 fill:#cba6f7,color:#11111b style M2 fill:#89b4fa,color:#11111b style E2 fill:#f9e2af,color:#11111b style D2 fill:#89b4fa,color:#11111b style V2 fill:#cba6f7,color:#11111b
Como técnica de optimización
Esta propiedad se puede explotar deliberadamente. La forma más útil es la guarda barata delante del cálculo caro.
// Malo: siempre depende de datosPesados
const resultado = memo(() => {
const pesado = procesar(datosPesados());
return visible() ? pesado : null;
});
// Bueno: mientras no sea visible, ni siquiera hay arista con datosPesados
const resultado = memo(() => {
if (!visible()) return null;
return procesar(datosPesados());
});
Las dos versiones producen el mismo valor. La diferencia está en el grafo: la primera lee datosPesados siempre, así que cualquier cambio en esos datos reejecuta el memo aunque esté invisible. La segunda solo teje esa arista cuando de verdad hace falta, y mientras visible sea falso el subgrafo entero de datosPesados queda desconectado.
Escalado a una aplicación, el patrón es potentísimo: una pestaña oculta, un acordeón cerrado o una fila fuera de la pantalla pueden desaparecer del grafo en vez de seguir recalculándose sin que se vea. Es la razón por la que las primitivas de flujo de control de estos motores —el Show de Solid, un v-if en Vue— tienen implicaciones de rendimiento muy distintas a ocultar con CSS.
Como corolario práctico: en una condicional compuesta, pon primero las condiciones que dependen de fuentes que cambian poco y son baratas de leer. El cortocircuito hará que las fuentes caras ni siquiera se lean, y por tanto ni siquiera se conecten. Es la misma disciplina que ordenar predicados en una consulta, aplicada al grafo.
El cortocircuito corta aristas
Los operadores &&, || y ?? no evalúan su lado derecho si el izquierdo decide. Como la evaluación es lo que teje aristas, el cortocircuito corta dependencias.
const puedeEditar = memo(() => sesionActiva() && usuario().rol === 'editor');
Si sesionActiva() devuelve false, usuario() no se llama y no hay arista con usuario. El memo depende de una sola fuente. Cuando la sesión se active, la siguiente evaluación leerá las dos y tejerá las dos.
Esto casi siempre es lo que quieres, y es una de las razones por las que este modelo se siente natural: el grafo refleja la lógica real. Pero produce una sorpresa concreta cuando el orden de los operandos es arbitrario. Estas dos expresiones son lógicamente equivalentes y tienen grafos distintos.
const a = memo(() => barato() && caro()); // arista con caro solo si barato es cierto
const b = memo(() => caro() && barato()); // arista con caro siempre
Inestabilidad y cómo evitarla
Los tres patrones que producen inestabilidad
El interruptor de alta frecuencia. Una condición que cambia muchas veces por segundo y que alterna entre dos ramas con dependencias muy distintas. Cada alternancia desata un conjunto de aristas y teje otro. Si cada rama tiene veinte dependencias, cada alternancia son ochenta operaciones de contabilidad, más el cómputo. Cuando la condición viene de un scroll o de un puntero, es un problema real.
El bucle con condicional dentro. Un cuerpo que recorre una colección y lee condicionalmente según cada elemento. El conjunto de aristas depende del contenido de la colección, así que cambia de forma impredecible y la contabilidad se vuelve significativa. El remedio suele ser partir el nodo: uno por elemento en vez de uno para el conjunto.
La lectura con efecto lateral en la condición. Si la condición llama a una función que a su vez lee fuentes distintas según el momento, las aristas se vuelven imposibles de predecir y de razonar. Es el equivalente reactivo del código no determinista.
Hay una manera de mirar todo esto que ordena el resto del track. Tu código de derivación es, además de un cálculo, un programa que describe el grafo; y el tracking automático es su intérprete. Cuando escribes un if, no estás escribiendo solo una bifurcación de cálculo: estás escribiendo una bifurcación en la construcción del grafo. Cuando escribes un &&, estás escribiendo una arista opcional. Cuando llamas a una función, estás inlineando el fragmento de grafo que esa función describe. Bajo esta lente, dos habilidades que parecían distintas resultan ser la misma: escribir código reactivo eficiente es escribir código cuyo grafo inducido tiene buena forma, y eso significa poco profundo, poco ramificado en los puntos calientes y con dependencias estables. Y hay un corolario incómodo que conviene aceptar pronto: la forma más legible de escribir un cálculo no siempre induce el mejor grafo. Extraer una condición a una variable local antes del if es más legible y destruye el cortocircuito; recorrer una colección en un solo memo es más limpio y crea un cuello de botella. Cuando entren en conflicto, no hay una regla general: hay que saber que existe la tensión y decidir con el perfil delante, sabiendo qué se está sacrificando.
Cuando quieres dependencias estables
A veces el redescubrimiento continuo no compensa y prefieres fijar las dependencias. Todos los motores ofrecen una vía, aunque no todos la anuncien igual.
En Solid, el helper on convierte una computación de tracking automático en una de dependencias declaradas, y además admite diferir la primera ejecución. En Vue, watch con una lista explícita de fuentes hace lo mismo. En Angular, se combina untracked para excluir lecturas que no deben crear aristas. En cualquier motor, envolver la parte que no quieres rastrear en la primitiva de leer sin rastrear consigue el mismo efecto de forma manual.
La regla práctica: usa dependencias declaradas cuando el conjunto sea estable y el cuerpo caro, y automáticas en todo lo demás.
- Escribe un memo que procese una colección grande y devuelva un resumen, con una bandera de visibilidad leída al final.
- Instrumenta el número de reejecuciones al escribir en la colección con la bandera desactivada.
- Reescríbelo poniendo la guarda al principio y vuelve a medir.
- Vuelca las aristas del memo en ambos casos y comprueba que el subgrafo entero se desconecta.