Saltar al contenido

Memo vs función simple vs signal

Tres formas de tener un valor derivado en Solid: una función normal, un createMemo o un signal sincronizado a mano. Cuándo el cacheo compensa —cálculo caro, muchos lectores, identidad estable— y cuándo solo añade un nodo inútil.

Tres formas de derivar

La clave contraintuitiva viene de la lección anterior: la función simple también es reactiva. Al leer doble() dentro de un efecto o del JSX, Solid rastrea el n() que hay dentro y suscribe al consumidor. La función no crea un nodo propio: inyecta sus dependencias en el ámbito que la llama, como si su cuerpo estuviera escrito ahí mismo.

TSX
const [n, setN] = createSignal(1);const doble = () => n() * 2; // reactiva, sin cachécreateEffect(() => console.log(doble())); // el effect depende de n

Esa inyección de dependencias tiene una virtud: la función aplana el grafo. No añade un nodo intermedio, así que para derivaciones triviales es más ligera que un memo y se lee como lo que es, una fórmula. El precio es que no cachea: si el mismo doble() aparece en diez lugares rastreados, se evalúa diez veces por cada cambio de n. Vistas lado a lado, las tres formas revelan su carácter:

TSX
const f = () => n() * 2;                    // recomputa en cada lecturaconst m = createMemo(() => n() * 2);        // recomputa por cambio, y cacheaconst [s, setS] = createSignal(n() * 2);    // NO se actualiza solo

Fíjate en la tercera: nace desincronizada. s vale el doble de n solo en el instante de crearse; en cuanto n cambie, s miente hasta que alguien la reescriba a mano. Ese es el defecto de fondo del signal derivado a mano, y ninguna cantidad de efectos lo cura del todo.

El cálculo del coste

Un memo no es gratis: ocupa memoria, ocupa una entrada en el grafo y añade una indirección. Solo compensa cuando evita más trabajo del que cuesta. Dos factores mandan.

  • Coste del cálculo. Si derivar es caro —ordenar un array, filtrar mil filas, parsear— recomputarlo en cada lectura duele. El memo lo hace una vez por cambio.
  • Número de lectores. Con un solo lector, memo y función recomputan lo mismo: una vez por cambio. El cacheo empieza a ganar cuando muchos consumidores comparten la derivación en el mismo tick.

La regla mental: el beneficio es, más o menos, el coste del cálculo multiplicado por los lectores menos uno, restado el coste fijo del nodo. Un arr().length con tres lectores no justifica un memo; un arr().filter(pesado).sort() con tres lectores, sí. Un memo lee arr una vez, ordena una vez y sirve el mismo resultado tres veces; la función ordenaría tres veces.

Ponle números para verlo claro. Si ordenar cuesta dos milisegundos y lo leen cinco vistas en el mismo fotograma, la función gasta diez milisegundos por cambio y el memo dos —una quinta parte—. Con un único lector, ambos gastan dos milisegundos y el memo solo añade el coste de mantener su nodo. El cruce es nítido: cuantos más lectores y más caro el cálculo, más rinde el memo; con pocos lectores y cálculo barato, el memo es puro lastre que ni la mejor intención justifica.

TSX
const caro = createMemo(() => ordenar(items())); // o: const caro = () => ordenar(items())// Un lector: memo y función empatan (más el coste del nodo)createEffect(() => usar(caro()));// Cinco lectores en el mismo tick: el memo ordena 1 vez; la función, 5

Por eso el número de lectores no es un detalle secundario: es la variable que decide. Cuenta cuántos consumidores comparten la derivación en un mismo tick antes de memoizar; si la respuesta es “uno”, casi siempre te sobra el memo.

Identidad estable: la tercera razón

Hay un motivo para memoizar que no va de velocidad bruta sino de referencia. Un memo devuelve la misma referencia hasta que su contenido cambia; una función devuelve un objeto nuevo en cada llamada. Aguas abajo, un <For>, un children o un efecto que compara por identidad dependen de esa estabilidad para no rehacer trabajo:

TSX
// Una función crearía un array nuevo cada vez que el For lo lee,// forzando reconciliaciones inútiles. El memo estabiliza la referencia:const visibles = createMemo(() => items().filter((i) => i.activo));<For each={visibles()}>{(i) => <Fila dato={i} />}</For>

Aquí el memo no ahorra un cálculo caro: ahorra que el <For> reciba una referencia distinta en cada acceso y remonte filas sin necesidad. Identidad estable es, junto con el corte de propagación de la lección 4, una razón de peso aunque el cálculo sea barato.

El mismo argumento vale para children y para efectos que comparan por identidad. Si aguas abajo alguien decide actuar solo cuando la referencia cambia, necesitas que esa referencia sea estable, y solo un memo —o un signal— te la da; una función te entrega un objeto nuevo en cada lectura y sabotea ese === de raíz:

TSX
// El efecto reordena el DOM solo cuando la clave de contenido cambia:const clave = createMemo(() => visibles().map((i) => i.id).join(","));createEffect(() => { clave(); reordenar(); });

La regla se resume así: si aguas abajo hay un === que decide si trabajar, arriba necesitas un memo. Una función te entrega una referencia nueva cada vez y nunca satisface ese === dos lecturas seguidas; el memo, sí, mientras el contenido no cambie.

El anti-patrón: derivar hacia un signal

La tentación de quien viene de otros modelos es guardar el valor derivado en su propio signal y mantenerlo al día con un efecto:

TSX
// MAL: estado derivado en un signalconst [doble, setDoble] = createSignal(0);createEffect(() => setDoble(n() * 2));

Esto duplica la fuente de la verdad. Introduce un tick extra —doble va siempre un paso por detrás de n—, rompe la garantía de consistencia del grafo y abre la puerta a que alguien escriba setDoble a mano y lo desincronice. Todo lo que este efecto hace, un memo lo hace mejor y en un tick:

TSX
// BIEN: una sola verdad, derivada y consistenteconst doble = createMemo(() => n() * 2);

¿Y si de verdad necesitas un derivado que también se pueda escribir desde fuera —un campo calculado por defecto que el usuario puede sobrescribir—? Ese caso existe y tiene solución propia, pero no es el createSignal más createEffect ingenuo: es un patrón deliberado que conserva una única fuente de la verdad y expone la escritura de forma controlada. La sincronización por efecto no es ese patrón; es su imitación rota.