Granularidad de la reactividad: grano fino contra grano grueso
Todo sistema reactivo elige una unidad mínima de invalidación: el nodo del grafo en los signals, o el componente entero en el modelo de re-render. Esa elección no es un detalle de implementación sino la decisión de rendimiento más determinante de la arquitectura, porque fija de antemano cuánto trabajo se hace por cada cambio y qué se paga por saber quién depende de qué. Esta lección compara ambos modelos por su coste asintótico real, distingue el trabajo de propagación del trabajo de render, y explica por qué la industria de 2026 converge hacia el grano fino sin abandonar el grano grueso.
Cuando un dato cambia, alguien tiene que decidir qué se recalcula. La respuesta a esa pregunta define la granularidad del sistema: la unidad mínima de invalidación. En el grano grueso, esa unidad es el componente: cambia un dato, se vuelve a ejecutar la función entera y se compara su salida con la anterior. En el grano fino, la unidad es el nodo del grafo: cambia un dato y solo se recalculan los nodos que lo leen, hasta llegar exactamente a las expresiones del DOM que dependen de él. Ambos modelos son correctos y ambos producen la misma interfaz; lo que cambia es la cantidad de trabajo por actualización y dónde vive el coste. Entender esa diferencia es dejar de tratar el rendimiento del estado como magia y empezar a tratarlo como contabilidad.
- Definir la granularidad como la unidad mínima de invalidación de un sistema reactivo.
- Comparar el coste asintótico del grano fino y del grano grueso por actualización.
- Separar el coste de propagar el cambio del coste de reconstruir la vista.
- Reconocer qué paga cada modelo en memoria, en arranque y en mantenimiento.
Dos formas de responder a un cambio
El modelo de grano grueso es el de React clásico y sus herederos. Un dato cambia, el framework marca el componente como sucio y vuelve a llamar a su función. Esa función devuelve una descripción nueva de la interfaz, y un algoritmo de reconciliación la compara con la descripción anterior para deducir qué operaciones aplicar al DOM. El estado no sabe quién lo lee: sabe en qué componente vive, y la unidad de invalidación es ese componente completo más, por defecto, todo su subárbol.
// grano grueso: el cambio invalida la funcion entera
function Panel() {
const [n, setN] = useState(0)
const etiqueta = formatear(n) // se recalcula en cada render
return <div>{etiqueta}<Hijo /></div> // Hijo tambien se re-ejecuta
}
El modelo de grano fino es el de los signals. Leer una señal dentro de un contexto reactivo registra una arista en un grafo de dependencias explícito. Cuando la señal se escribe, el sistema recorre solo el cono de nodos alcanzables desde ella y recalcula únicamente esos. No hay función de componente que reejecutar, no hay descripción intermedia de la interfaz que comparar: la expresión que produce el texto de un nodo del DOM es, ella misma, un nodo del grafo.
// grano fino: el cambio invalida la expresion, no el componente
const n = signal(0)
const etiqueta = computed(() => formatear(n()))
effect(() => { nodoTexto.data = etiqueta() }) // solo esto corre al escribir n
La diferencia entre los dos fragmentos no está en la sintaxis sino en quién guarda la información de dependencia. En el primero, esa información no se guarda en ninguna parte: se reconstruye implícitamente cada vez, ejecutando el código y observando qué sale. En el segundo, se guarda de forma explícita en el momento de la lectura, y se consulta después. Todo lo demás, incluida la sensación de que un modelo es más declarativo o más mágico que el otro, se deriva de esa única decisión.
En un sistema de grano fino sigue habiendo componentes: son funciones de organización que se ejecutan una sola vez, durante la creación, para construir el grafo y el DOM. Después de esa ejecución inicial nunca vuelven a correr. Lo que desaparece no es el componente como abstracción de código, sino el componente como unidad de invalidación. Confundir ambas cosas lleva a la conclusión errónea de que el grano fino renuncia a la modularidad, cuando lo único a lo que renuncia es a reejecutar la función cada vez que un dato cambia.
Lo que cuesta cada modelo
La comparación honesta no es cuál es más rápido, sino qué paga cada uno y cuándo. El grano grueso paga por actualización un trabajo proporcional al tamaño del subárbol invalidado, aunque el cambio afecte a un solo carácter en pantalla. El grano fino paga por actualización un trabajo proporcional al número de dependientes reales del dato, que suele ser una constante pequeña e independiente del tamaño de la aplicación.
Coste por actualización
Grano grueso: proporcional al subárbol reejecutado más la reconciliación de su salida. Grano fino: proporcional al cono de dependientes del nodo escrito.
Coste en memoria
Grano fino: aristas y nodos explícitos, un objeto por dependencia rastreada. Grano grueso: no guarda grafo, pero mantiene el árbol anterior completo para poder compararlo.
Coste de arranque
Grano fino: construir el grafo y suscribir efectos durante la creación. Grano grueso: un primer render que ya es el caso normal, sin estructura extra que montar.
Coste cognitivo
Grano grueso: reglas de memoización explícitas como memo y listas de dependencias. Grano fino: cuidado con dónde se lee la señal, porque leerla fuera del contexto reactivo rompe el rastreo.
La asimetría clave está en cómo escala cada coste con el tamaño de la aplicación. En grano grueso, añadir un componente intermedio entre el estado y su consumidor añade trabajo a cada actualización que atraviese esa rama, porque la reejecución se propaga hacia abajo por estructura. En grano fino, añadir capas de componentes no añade nada al coste de propagación, porque el grafo conecta el productor con el consumidor directamente y la jerarquía de componentes es irrelevante después de la creación. Por eso el grano grueso necesita memo, comparadores y useMemo como herramientas de contención: son formas manuales de podar un cono de invalidación que por defecto es demasiado ancho.
Hay un segundo eje que la comparación suele ignorar y que en la práctica decide más que el primero: dónde vive el estado dentro del árbol. En grano grueso, subir un estado a un ancestro común para compartirlo entre dos hojas convierte a ese ancestro en el punto de invalidación, y a partir de ahí cada escritura arrastra a todo el subárbol que cuelga de él, incluidos los hermanos que no lo leen. En grano fino, la posición del estado en el árbol es irrelevante para el coste, porque la propagación no viaja por la estructura sino por las aristas del grafo. Esta es la razón por la que el patrón de elevar el estado, tan razonable como organización del código, se convierte en el mayor generador de trabajo redundante de las aplicaciones de grano grueso.
flowchart TD S[escritura del dato] --> G1[grano grueso marca el componente sucio] S --> G2[grano fino marca el nodo del grafo] G1 --> R1[reejecuta funcion y subarbol] R1 --> D1[reconcilia salida contra la anterior] D1 --> P[parche minimo al DOM] G2 --> R2[recalcula solo los dependientes reales] R2 --> P
Propagar no es renderizar
Un error frecuente al perfilar es sumar en un solo número dos costes que se comportan de forma muy distinta. La propagación es el trabajo de averiguar qué cambió y avisar a quien corresponda: recorrer el grafo o marcar componentes sucios. El render es el trabajo de convertir esos valores en píxeles: construir descripciones, compararlas y escribir en el DOM. El grano fino reduce drásticamente el primero y elimina buena parte del segundo, porque no hay descripción intermedia que reconciliar. El grano grueso mantiene ambos costes acoplados: propagar implica reejecutar, y reejecutar implica reconciliar.
// el mismo cambio, dos facturas distintas
// grano grueso: 1 marca + N reejecuciones + N comparaciones + 1 parche
// grano fino: 1 marca + K recalculos del cono + 1 parche
// con K normalmente mucho menor que N y ademas independiente de la profundidad
Esta separación explica por qué las optimizaciones de grano grueso son casi todas defensivas. memo no acelera nada: corta la propagación estructural comparando propiedades para evitar una reejecución que no habría cambiado nada. Lo mismo hacen useMemo sobre un valor derivado o un selector con igualdad personalizada. Son parches que reconstruyen a mano, componente a componente, la información que un grafo de dependencias tiene de forma nativa. El React Compiler de 2026 automatiza esa reconstrucción insertando memoización derivada del análisis estático, lo que reduce el coste cognitivo pero no cambia la naturaleza del modelo: la unidad sigue siendo el componente.
Instrumentar esa terna es más fácil de lo que parece y no requiere herramientas especiales. Basta con un contador por función y un contador global de operaciones sobre el DOM para obtener una fotografía que sobrevive a cambios de máquina y de navegador.
// instrumentacion minima para medir granularidad efectiva
const ejecuciones = new Map<string, number>()
function contar(nombre: string): void {
ejecuciones.set(nombre, (ejecuciones.get(nombre) ?? 0) + 1)
}
// se llama al principio de cada componente o expresion reactiva
// tras una interaccion: cuantas entradas hay y cual es la suma total
Un resultado sano tiene dos propiedades. La primera es que la suma no cambie cuando se añaden componentes que no leen el dato modificado; si crece, la granularidad efectiva es peor que la nominal y hay estructura interfiriendo en la propagación. La segunda es que la relación entre funciones ejecutadas y operaciones sobre el DOM sea razonable; decenas de ejecuciones para un solo parche indican que casi todo el trabajo se está descartando en la reconciliación.
Antes de discutir si un modelo es más rápido, cuenta. Instrumenta una actualización real y responde a tres preguntas: cuántas funciones se ejecutaron, cuántas comparaciones se hicieron y cuántas operaciones llegaron al DOM. Esa terna describe la granularidad efectiva de tu aplicación mucho mejor que cualquier medición de milisegundos, porque los milisegundos dependen de la máquina y el cono no. Si el número de funciones ejecutadas crece cuando añades un componente que no toca el dato, tienes un problema de granularidad, no de velocidad.
Un grafo explícito cuesta memoria y disciplina. Cada dependencia rastreada es un objeto vivo que debe crearse, recorrerse y liberarse; un componente con miles de señales pequeñas puede gastar más en mantener aristas que lo que ahorra en recálculos. Además, el rastreo automático depende de leer la señal dentro del contexto reactivo: desestructurar un objeto reactivo, pasar el valor ya leído a una función auxiliar o capturarlo en una variable rompe la arista silenciosamente y produce interfaces que dejan de actualizarse sin ningún error visible. El grano grueso falla ruidosamente por trabajo de más; el grano fino falla en silencio por aristas de menos.
La convergencia de 2026
El panorama actual ha dejado de ser una guerra de bandos. Solid, Vue, Svelte 5, Angular y Preact usan grafos de grano fino como motor de actualización. React mantiene el modelo de componente pero incorpora un compilador que memoiza automáticamente y participa en el grupo de trabajo de Signals de TC39, cuya propuesta define un grafo estándar con rastreo automático y propagación sin glitches. La dirección es clara: el grano fino gana como mecanismo de propagación, y el componente sobrevive como unidad de organización, de carga y de frontera de servidor.
// grano fino dentro de un mundo de grano grueso
const store = crearStore(estadoInicial) // fuente externa al arbol
function Fila({ id }: { id: string }) {
// solo este componente se invalida cuando cambia ESA entrada
const item = useStore(store, s => s.porId[id])
return <li>{item.nombre}</li>
}
// el contenedor se suscribe solo al array de identificadores
// insertar o editar un item no reejecuta las filas ajenas
La lectura práctica es que la granularidad ha dejado de ser una propiedad global del framework para convertirse en una decisión local del programador. En React puedes aislar una zona caliente con un store externo suscrito por selector y obtener propagación casi de grano fino dentro de un mundo de grano grueso. En Solid puedes agrupar deliberadamente varias señales en un solo objeto para reducir el número de aristas cuando el cono es amplio y el coste de mantener el grafo domina. La pregunta útil ya no es qué modelo usa mi framework, sino qué granularidad merece esta parte concreta de mi aplicación.
Conviene cerrar con una advertencia sobre el sentido de esta comparación. El grano fino reduce el trabajo por actualización, pero el trabajo por actualización solo domina cuando las actualizaciones son frecuentes y los conos son amplios. Una pantalla que cambia tres veces por minuto no mejora de forma perceptible al cambiar de modelo, y en cambio paga en memoria y en riesgo de aristas rotas. La granularidad es una herramienta para zonas calientes, no una virtud que haya que maximizar en todas partes, y elegirla bien exige saber antes cuál es la zona caliente, que es justamente lo que se aprende a averiguar en la última lección de este nivel.
Todo sistema reactivo se enfrenta a la misma pregunta cuando un dato cambia: quién depende de esto. Solo hay dos maneras de responderla, y la granularidad no es más que la elección entre ellas. El grano fino responde por adelantado: paga durante la lectura, registrando cada dependencia en el momento en que ocurre, y construye una estructura persistente que después consulta en tiempo proporcional al número de dependientes reales. El grano grueso responde a posteriori: no guarda nada, y cuando llega el cambio deduce la respuesta reejecutando el código y comparando resultados, lo que le cuesta un trabajo proporcional al tamaño de la región reejecutada. Es la misma disyuntiva que atraviesa toda la informática, la que separa un índice de un escaneo completo, la memoización del recálculo, la compilación de la interpretación: pagar por adelantado para consultar barato, o no pagar nada y pagar en cada consulta. Ninguna de las dos es superior en abstracto, porque la respuesta depende de la relación entre la frecuencia de las escrituras, el tamaño de la región afectada y el coste de mantener la estructura. Lo que sí es superior es entender que estás eligiendo. El ingeniero que ve el memo de React como un truco para ir más rápido nunca sabrá dónde ponerlo; el que lo ve como una forma manual y local de reconstruir el índice que el grano fino mantiene automáticamente sabrá exactamente dónde hace falta, dónde sobra, y cuándo el problema no es la memoización sino haber puesto el estado en el lugar equivocado del árbol.
La consecuencia práctica de todo lo anterior cabe en una frase: la granularidad no se elige una vez al empezar el proyecto, se negocia zona por zona con datos delante. Y el criterio para negociarla es siempre el mismo, comprobar si el coste de una actualización guarda proporción con el tamaño de lo que esa actualización cambia.
- Toma un componente de grano grueso con tres niveles de anidamiento e instrumenta cada función para registrar sus ejecuciones; cambia un dato de la hoja más profunda y cuenta cuántas funciones corrieron.
- Reescribe el mismo caso con señales y una expresión reactiva por nodo de texto; vuelve a contar y calcula el factor de reducción.
- Añade un componente intermedio que no lee el dato y repite ambas mediciones; comprueba cuál de los dos modelos ve crecer su coste.
- Aplica
memoal componente intermedio en la versión de grano grueso y verifica cuánto del cono recuperas y qué comparaciones nuevas pagas a cambio. - En la versión de grano fino, rompe deliberadamente una arista leyendo la señal fuera del contexto reactivo y describe exactamente cómo se manifiesta el fallo.