wandres.dev
DERIVACIÓN · memos y selectores

Memoización: cachear la derivación cara

Derivar es correcto siempre, pero no siempre es barato. Cuando la función que proyecta el estado fuente es costosa —ordenar miles de filas, recalcular un agregado— recomputarla en cada render o en cada lectura desperdicia trabajo. La memoización cachea el resultado y lo reutiliza mientras sus entradas no cambien, comparadas por referencia. Su contrato, sus dos motivos reales y por qué el React Compiler de 2026 reescribe la pregunta de cuándo memoizar a mano.

⏱ 17 min

La lección anterior estableció que derivar siempre es correcto; esta añade el matiz que faltaba: derivar no siempre es barato. Si la función que proyecta el estado fuente ordena diez mil filas, recalcula un agregado o transforma una estructura profunda, recomputarla en cada render de React o en cada lectura de una señal es trabajo tirado a la basura cuando sus entradas no han cambiado. La memoización resuelve exactamente eso: cachea el último resultado y lo devuelve tal cual mientras las entradas sigan siendo las mismas, recomputando solo cuando alguna cambia. Es un intercambio de espacio por tiempo con un contrato preciso —qué cuenta como cambio— y dos motivos para aplicarlo que conviene no confundir. Y es, en 2026, un terreno que el compilador está reescribiendo: entender qué memoiza una máquina y qué te queda a ti es parte del oficio.

🎯 Al terminar esta lección sabrás
  • Modelar la memoización como un intercambio de espacio por tiempo con una cache de tamaño uno.
  • Interiorizar el contrato de la igualdad por referencia y por qué gobierna cada acierto de cache.
  • Separar los dos motivos para memoizar: ahorrar cómputo y estabilizar referencias aguas abajo.
  • Decidir cuándo memoizar a mano en la era del React Compiler y de la reactividad de grano fino.

El coste de derivar y la promesa de la cache

Una derivación pura tiene una propiedad que la hace cacheable sin riesgo: la transparencia referencial. Como f(fuente) devuelve siempre lo mismo para las mismas entradas y no provoca efectos, guardar su resultado y reutilizarlo es indistinguible de volver a calcularlo, salvo por lo que importa —el tiempo—. La memoización explota justo eso: recuerda el par de entradas más reciente junto a su salida, y ante una nueva invocación compara las entradas nuevas con las guardadas; si coinciden, devuelve la salida cacheada sin ejecutar la función. La inmensa mayoría de los memos usan una cache de tamaño uno: recuerdan solo la última llamada, porque en un flujo reactivo lo habitual es recomputar una y otra vez con las mismas entradas entre cambios.

Ese tamaño uno es una decisión de diseño, no una limitación tonta. Una cache de una sola entrada nunca crece sin control, no necesita política de expulsión y acierta perfectamente en el caso dominante: renders repetidos sin cambios reales en las dependencias. El precio es que alternar entre dos conjuntos de entradas hace que la cache falle en cada llamada —el problema que aparecerá con fuerza en los selectores compartidos entre elementos de una lista—, pero para una derivación local es exactamente el comportamiento que quieres.

// useMemo: cachea una derivacion cara mientras sus entradas no cambien
const ordenadas = useMemo(
  () => filas.slice().sort(comparador), // funcion pura y costosa
  [filas, comparador] // recalcula solo si cambia una dep por referencia
);

El contrato: igualdad por referencia

El corazón de toda memoización es la definición de cambio, y en el ecosistema de JavaScript esa definición es la igualdad por referencia, la que implementa Object.is. El array de dependencias de useMemo no compara el contenido de sus entradas, compara sus identidades: dos objetos con los mismos campos son entradas distintas si son objetos distintos en memoria. Esta elección es deliberada, porque comparar por referencia es de coste constante mientras que comparar en profundidad podría costar tanto como la propia derivación que se quiere evitar. El corolario es la regla de oro de la memoización: la cache solo acierta si las entradas mantienen su referencia entre llamadas.

De ahí nace el error más común y más difícil de ver: declarar como dependencia un objeto, un array o una función creados en línea dentro del render. Cada render fabrica una referencia nueva, la comparación siempre falla y el memo recomputa cada vez, convertido en puro sobrecoste sin ningún beneficio. La corrección es doble: o se derivan las dependencias de valores primitivos estables, o se estabiliza la referencia con su propio memo —useMemo para valores, useCallback para funciones—.

// Antipatron: la dependencia es un objeto nuevo en cada render
const opciones = { orden: "asc" };             // referencia distinta siempre
const datos = useMemo(() => procesar(filas, opciones), [filas, opciones]);
// resultado: la cache nunca acierta y el memo recomputa en cada render

// Correccion: depende de primitivos estables, no de un objeto recien creado
const datos = useMemo(() => procesar(filas, { orden }), [filas, orden]);
ℹ️
Hay dos motivos para memoizar, y el segundo es el que se olvida

El primero es obvio: evitar recomputar una función cara. El segundo es sutil y a menudo el verdadero: garantizar que el resultado conserve su referencia cuando las entradas no cambian. Un array derivado que se recrea en cada render, aunque su contenido sea idéntico, es una referencia nueva que dispara el re-render de los hijos memoizados que lo reciben y reejecuta los useEffect que lo tienen como dependencia. Memoizar aquí no ahorra cómputo, estabiliza la identidad, y esa estabilidad es lo que corta la propagación de renders aguas abajo.

Cuándo memoizar y cuándo no

Fuera de React, la pregunta de cuándo memoizar apenas se plantea, porque los primitivos de derivación memoizan por defecto y rastrean sus dependencias solos. El computed de Vue y de Angular, el createMemo de Solid, el rune $derived de Svelte 5, el Signal.Computed del TC39 y el computed de MobX no llevan array de dependencias: descubren qué señales leen al ejecutar su cuerpo y cachean el resultado hasta que una de ellas cambia. Casi todos son además perezosos: no calculan al declararse, sino la primera vez que alguien lee el valor estando sucio, de modo que una derivación que nadie observa no cuesta nada.

// Vue / Angular / Solid / Signals TC39: sin array de dependencias
// La dependencia se rastrea sola al leer las senales dentro del cuerpo
const ordenadas = computed(() => filas.value.slice().sort(comparador));
// Evaluacion perezosa: solo se calcula al leerse y solo si algo cambio
flowchart TD
R[alguien lee el memo] --> Q{alguna entrada cambio}
Q -->|no| C[devuelve el valor cacheado]
Q -->|si| E[recalcula la funcion pura]
E --> S[guarda el resultado en la cache]
S --> C
style C fill:#a6e3a1,color:#11111b
style E fill:#fab387,color:#11111b

En React, el cálculo ha cambiado con la llegada del React Compiler a estable. El compilador analiza el código en tiempo de build y memoiza automáticamente valores y componentes según las dependencias que infiere, haciendo innecesaria buena parte del useMemo y useCallback escritos a mano. La guía de 2026 se invierte: por defecto, deja que el compilador memoice y no ensucies el código con memos manuales; reserva la memoización explícita para lo que el compilador no puede ver o garantizar —una identidad estable que cruza una frontera que no controla, una dependencia externa, un caso medido donde su heurística no basta—. Escribir useMemo por reflejo dejó de ser prudencia y pasó a ser ruido.

⚠️
Memoizar de más también cuesta: la cache no es gratis

Toda memoización tiene su propio precio: guardar las entradas anteriores, compararlas en cada llamada y ocupar memoria con el resultado cacheado. Cuando la derivación es trivial —sumar dos números, formatear una cadena corta—, ese sobrecoste supera al del cálculo que evita, y memoizar sale a pérdida neta además de enturbiar el código. La memoización rinde cuando el cómputo es caro frente a la comparación de igualdad, o cuando lo que persigues es la estabilidad de referencia. Fuera de esos dos casos, recomputar es más barato y más claro.

Estabilidad de referencia y semántica comparada

El segundo motivo para memoizar —estabilizar una referencia— sostiene el rendimiento de las jerarquías grandes y merece un ejemplo propio. Cuando un componente pasa un objeto o un array derivado a un hijo envuelto en React.memo, o lo declara como dependencia de un efecto, lo que decide si el hijo se re-renderiza o el efecto se reejecuta no es el contenido de ese valor, sino su identidad. Recrearlo en cada render, aunque su contenido sea idéntico, dispara ambas cosas; memoizarlo conserva su referencia y corta la propagación en seco.

// Sin memoizar: config es una referencia nueva en cada render
function Panel({ tema }) {
  const config = { tema, densidad: "compacta" }; // identidad nueva siempre
  return <Grafico config={config} />;             // Grafico memoizado se re-renderiza igual
}

// Memoizado: config conserva su identidad mientras tema no cambie
function Panel({ tema }) {
  const config = useMemo(() => ({ tema, densidad: "compacta" }), [tema]);
  return <Grafico config={config} />;             // ahora si se salta el re-render
}

Este patrón revela por qué la semántica de cada primitivo importa tanto como su existencia. useMemo recalcula durante el render del componente, acoplado al ciclo de React, con una cache que vive solo mientras el componente esté montado. Los computed de Vue, Angular y MobX, el createMemo de Solid y el Signal.Computed del TC39 son perezosos y viven fuera de todo ciclo de componente: rastrean sus dependencias solos y solo evalúan al leerse estando sucios. Esa distinción —memo atado al render frente a memo perezoso de grano fino— separa las dos grandes familias de reactividad de 2026.

⚛️

useMemo, atado al render

Recalcula en el render cuando una dependencia cambia por referencia, y su cache dura lo que dura el montaje. En 2026, el React Compiler lo inserta por ti casi siempre.

🌿

computed, perezoso y cacheado

Vue, Angular, Solid y MobX rastrean las dependencias solos y solo evalúan al leerse estando sucios. No hay array que declarar ni nada que sincronizar a mano.

📐

Signal.Computed, sin glitches

La propuesta del TC39 estandariza un memo perezoso, cacheado y libre de glitches, con corte por igualdad de serie. Es el sustrato hacia el que converge el ecosistema.

Ninguno de estos primitivos compara en profundidad por defecto: todos apuestan por la igualdad por referencia porque es de coste constante. Cuando de verdad necesitas comparar contenido —un memo cuya entrada llega como objeto nuevo pero equivalente desde una capa que no controlas— la salida es una función de igualdad explícita, como la opción equals de createMemo de Solid, asumiendo a conciencia el coste de esa comparación a cambio de detener la propagación.

// Solid: una igualdad a medida corta la propagacion cuando el valor no cambia de verdad
const resumen = createMemo(
  () => derivar(fuente()),
  undefined,
  { equals: (a, b) => a.clave === b.clave } // compara por contenido, no por referencia
);
Primitivo Cuándo evalúa Igualdad por defecto
useMemo en el render, si cambian sus deps referencia con Object.is
computed de Vue y Angular al leerse, si está sucio referencia, con corte
createMemo de Solid al leerse, si está sucio referencia u opción equals
Signal.Computed del TC39 al leerse, si está sucio referencia, sin glitches
📝
La igualdad por defecto y la igualdad a medida resuelven problemas distintos

La igualdad por referencia es la elección correcta la mayoría de las veces: barata, predecible y suficiente cuando tú controlas la estabilidad de tus entradas. La igualdad a medida existe para el resto —entradas que no controlas, valores estructurales que se recrean equivalentes— y su precio es una comparación que corre en cada evaluación. Elegirla sin necesidad reintroduce, disfrazado, el mismo coste que la memoización venía a evitar.

Memoizar es apostar a que la identidad predice la igualdad

La memoización parece una técnica de rendimiento, pero en el fondo es una apuesta epistemológica sobre cómo saber, barato, que un resultado no ha cambiado. La verdad la daría comparar la salida nueva con la vieja, pero eso exige calcular la salida nueva, que es justo lo que queremos evitar; así que la memoización sustituye esa pregunta cara por una barata: ¿han cambiado las entradas, medidas por referencia? Toda la disciplina se sostiene sobre la validez de esa sustitución, y de ahí salen sus dos reglas de oro. La primera es la pureza: solo puedes cachear una función cuyo resultado dependa únicamente de sus entradas, porque si lee un reloj, un aleatorio o una variable mutable de fuera, unas entradas iguales ya no garantizan una salida igual y la cache empieza a mentir. La segunda es la estabilidad de referencia: como la señal de cambio es la identidad y no el contenido, tu deber es que las cosas que no cambian conserven su referencia, porque una referencia nueva con contenido idéntico es, para el sistema, un cambio, y basta ese falso positivo para que la cache falle en cadena y la propagación se dispare aguas abajo. Comprender esto reordena el instinto: el enemigo del rendimiento reactivo no suele ser el cálculo caro que olvidaste memoizar, sino la referencia inestable que hace fallar todas las caches que dependían de ella. Por eso el futuro del ecosistema tiende a quitarte esta decisión de las manos —el compilador que memoiza solo, el grafo de señales de grano fino que sabe exactamente qué cambió—, no porque memoizar sea difícil de escribir, sino porque razonar sobre identidad y pureza en cada punto es difícil de sostener sin equivocarse. Tu trabajo, cada vez más, no es colocar memos, sino mantener limpias la pureza de tus derivaciones y la estabilidad de tus referencias, y dejar que la máquina haga el resto.

⚔️ Mide antes de memoizar
  1. Toma una derivación cara real —un ordenamiento o un agregado sobre miles de elementos— y compara su tiempo con y sin useMemo usando el perfilador, en lugar de suponerlo.
  2. Busca en tu código un memo cuya dependencia sea un objeto o una función creados en línea, y demuestra que su cache nunca acierta.
  3. Encuentra un useMemo cuyo único propósito no sea ahorrar cómputo sino estabilizar una referencia que reciben hijos memoizados, y explica por qué borrarlo dispararía re-renders.
  4. Activa el React Compiler en un módulo y elimina los useMemo que pasan a ser redundantes; verifica con el perfilador que el rendimiento se mantiene.
  5. Reescribe una derivación con computed o createMemo y observa que la memoización y el rastreo de dependencias dejan de ser responsabilidad tuya.