React Compiler: memoización automática
El caso híbrido: un compilador que no cambia de familia sino que inserta la memoización que antes se escribía a mano, reduciendo el subárbol reejecutado sin adoptar tracking automático.
React Compiler es el ejemplo más interesante del nivel porque hace algo distinto a los otros dos: no elimina sintaxis ni genera plantillas, sino que inserta la maquinaria de memoización que antes escribía el programador. No convierte React en un motor de grano fino; reduce el tamaño del subárbol que se reejecuta, que es la variable que dominaba el coste según el nivel 1. Alcanzó la versión 1.0 estable en octubre de 2025.
- Entender qué transforma React Compiler y qué deja intacto.
- Situar la memoización automática en el eje de granularidad del nivel 1.
- Reconocer los límites de su análisis y las salidas de emergencia.
- Comparar la memoización compilada con el tracking automático.
Qué hace
React Compiler analiza el código de un componente, determina qué valores dependen de qué, y emite una versión que cachea los resultados intermedios entre renderizados. El componente sigue reejecutándose entero; lo que cambia es que las partes cuyo resultado no ha variado devuelven el valor cacheado en lugar de recalcularlo.
// Lo que escribes
function Panel({ elementos, filtro }) {
const visibles = elementos.filter(e => e.tipo === filtro);
const total = visibles.reduce((s, e) => s + e.precio, 0);
return <Lista datos={visibles} total={total} />;
}
// La forma de lo que se genera, muy esquematizada
function Panel({ elementos, filtro }) {
const c = usarCache(4);
let visibles;
if (c[0] !== elementos || c[1] !== filtro) {
visibles = elementos.filter(e => e.tipo === filtro);
c[0] = elementos; c[1] = filtro; c[2] = visibles;
} else visibles = c[2];
// ... lo mismo para total y para el elemento resultante
}
Es exactamente lo que hacía a mano quien envolvía cada cálculo en una memoización, con dos mejoras: el compilador no se equivoca con las dependencias y cachea también los elementos de la salida, cosa que a mano casi nadie hacía porque era demasiado verboso.
Dónde encaja en el eje del nivel 1
Vuelve al análisis de coste del nivel 1. El coste por actualización de un motor de reconciliación era proporcional a S, el tamaño del subárbol reejecutado, frente a K, lo que de verdad cambia. La ventaja del grano fino era que S se reducía a K.
React Compiler no cambia de familia: sigue reejecutando y comparando. Lo que hace es reducir el trabajo dentro de S. La función del componente sigue corriendo, pero sus cálculos internos se saltan si sus entradas no cambiaron, y sobre todo, los elementos devueltos son los mismos objetos, lo que hace que la comparación de los hijos termine inmediatamente sin descender.
Ese último efecto es el importante: al devolver los mismos objetos, el reconciliador puede podar subárboles enteros en lugar de compararlos. En la práctica, S se acerca bastante a K sin que nadie haya escrito una sola memoización.
flowchart TB
A[cambia una propiedad] --> B[el componente se reejecuta]
B --> C{sus entradas cambiaron}
C -->|no| D[devuelve los objetos cacheados]
D --> E[el reconciliador poda el subarbol]
C -->|si| F[recalcula y produce objetos nuevos]
F --> G[el reconciliador desciende]
style A fill:#89b4fa,color:#11111b
style B fill:#f9e2af,color:#11111b
style C fill:#f9e2af,color:#11111b
style D fill:#a6e3a1,color:#11111b
style E fill:#a6e3a1,color:#11111b
style F fill:#fab387,color:#11111b
style G fill:#fab387,color:#11111bLos límites de su análisis
El compilador tiene que ser conservador donde no puede probar que una transformación es segura, y hay tres situaciones donde se retira.
Mutación durante el renderizado. Si el código muta un objeto que también lee, el compilador no puede garantizar que cachear sea correcto y deja esa parte sin transformar.
Código que rompe las reglas del modelo. Si un componente incumple las reglas sobre dónde se pueden llamar los hooks o sobre la pureza del renderizado, el análisis no aplica. Por eso la adopción va acompañada de reglas de lint que detectan esas violaciones; desde la versión 1.0, las reglas alimentadas por el compilador se distribuyen en los presets recomendados del plugin de lint de hooks.
Cuando el programador lo desactiva. Existe una directiva para excluir un componente concreto del compilador, pensada como salida de emergencia mientras se arregla el código que impide la transformación.
Una expectativa común y equivocada es que el compilador hará funcionar código que hoy es frágil. Hace lo contrario: al necesitar probar que las transformaciones son seguras, expone las violaciones que antes pasaban desapercibidas. La experiencia de adopción típica no es que todo mejore mágicamente, sino que aparecen avisos en los sitios donde el código incumplía reglas que ya existían. Arreglar esos sitios es el trabajo, y suele merecer la pena por sí mismo.
Comparado con el tracking automático
Merece la pena poner las dos técnicas lado a lado, porque resuelven el mismo problema desde extremos opuestos.
| memoización compilada | tracking automático | |
|---|---|---|
| cuándo se determinan las dependencias | en compilación | en ejecución |
| precisión | conservadora, aproxima por arriba | exacta para cada ejecución |
| dependencias condicionales | no las aprovecha | las aprovecha por completo |
| coste en ejecución | comparaciones en cada render | tejido de aristas en cada ejecución |
| estructura persistente | una caché por instancia de componente | el grafo entero |
| qué pasa si el análisis falla | se retira y no transforma | no aplica |
La fila decisiva es la tercera. Un análisis en tiempo de compilación tiene que suponer que todas las ramas se pueden ejecutar, así que las dependencias de un cálculo condicional incluyen las de todas las ramas. El tracking automático solo teje las que la ejecución recorrió. Es la diferencia que cuantificamos en la lección 1 de este nivel.
A cambio, la memoización compilada no mantiene ningún grafo entre renderizados: solo un array de caché por instancia de componente, que se descarta con ella. Sin aristas que mantener, sin fugas de suscripción, sin árbol de dueños. Es la ventaja de la familia 1 que enumeramos en el nivel 1, conservada intacta.
Poner React Compiler junto a los compiladores de las dos lecciones anteriores deja ver algo que la conversación pública sobre frameworks casi nunca hace explícito: las dos familias están convergiendo hacia el mismo punto por caminos opuestos, y el punto de encuentro es reducir el trabajo por actualización a lo que de verdad cambió. La familia de reconciliación empezó sin ninguna información sobre dependencias y está añadiéndola con un compilador, aceptando la aproximación conservadora a cambio de no mantener ningún grafo. La familia de grano fino empezó con información exacta de dependencias y está usando compiladores para quitarle el coste sintáctico y el coste de construcción, aceptando mantener un grafo a cambio de precisión. Ninguna de las dos va a llegar donde está la otra: la reconciliación no puede alcanzar la precisión de las dependencias condicionales sin adoptar tracking, y el grano fino no puede desprenderse del grafo sin perder esa misma precisión. Lo que sí ha pasado, y es lo relevante para quien tenga que elegir en 2026, es que la distancia entre ambas se ha estrechado mucho en el caso medio. Eso desplaza el peso de la decisión hacia todo lo demás: la ergonomía, el modo de fallo con el que prefieres convivir, el ecosistema y lo que ya sabe tu equipo. Que es, precisamente, la conclusión a la que llegamos al final del nivel 1 antes de tener ninguno de estos datos.
- Escribe un componente con un cálculo caro y varios hijos, sin memoizar nada a mano.
- Instrumenta contadores de ejecuciones en el cálculo y en cada hijo.
- Cambia una propiedad que no afecta al cálculo y cuenta las ejecuciones con y sin compilador.
- Añade una mutación durante el renderizado y comprueba si el compilador se retira de esa parte.