El eje de la granularidad
Grano fino y grano grueso no son dos categorías sino un eje continuo: el tamaño de la unidad que el motor invalida y vuelve a producir. Dónde se sitúa cada motor y qué determina esa posición.
La distinción entre reactividad de grano fino y de grano grueso se suele contar como una guerra de bandos, y es en realidad una magnitud medible: el tamaño de la unidad mínima que el motor invalida cuando algo cambia. Esa unidad puede ser la aplicación entera, un componente, una expresión o un atributo del DOM, y de su tamaño se deduce mecánicamente todo lo demás: el coste por actualización, el consumo de memoria, y qué errores vas a cometer.
- Definir granularidad como el tamaño de la unidad de invalidación.
- Situar en el eje los modelos que existen, del más grueso al más fino.
- Deducir el coste por actualización a partir de la posición en el eje.
- Reconocer que la granularidad efectiva la fija el código, no el framework.
La unidad de invalidación
Cuando escribes un valor, el motor marca algo como inválido y vuelve a producirlo. La pregunta que define todo es: ¿qué es ese algo?
En el extremo más grueso está la aplicación entera. Es el modelo de un formulario de servidor clásico: cambias un campo, envías, y el servidor devuelve la página completa. La unidad de invalidación es el documento.
Un paso más fino: el componente. Cambias un valor del que depende un componente y se vuelve a ejecutar la función del componente entera, produciendo una descripción nueva de su salida. Es el modelo de React y de la mayoría de la familia 1.
Más fino: la expresión. Cambias un valor y solo se reevalúan las expresiones concretas que lo leyeron. La función del componente no vuelve a correr. Es el modelo de Solid, y el de Vue en su modo Vapor.
Y el extremo teórico: el atributo o el carácter. Cambias un valor y se reescribe exactamente un textContent o exactamente una propiedad de un elemento. En la práctica los motores de expresión ya llegan aquí, porque una expresión reactiva típica tiene como salida justamente un atributo o un nodo de texto.
flowchart LR A[documento entero] --> B[componente] B --> C[expresion] C --> D[atributo o nodo de texto] style A fill:#f38ba8,color:#11111b style B fill:#f9e2af,color:#11111b style C fill:#89b4fa,color:#11111b style D fill:#a6e3a1,color:#11111b
El eje va de izquierda a derecha, y moverse a la derecha significa lo mismo siempre: menos trabajo por actualización, más contabilidad que mantener. No hay ningún punto del eje que sea gratis.
Qué determina la posición
Un motor no elige su granularidad libremente: se la impone su decisión sobre cómo descubre las dependencias.
Un motor que no rastrea dependencias no puede ser fino. Si no sabes qué expresión leyó qué valor, la unidad más pequeña que puedes invalidar con seguridad es la más pequeña de la que puedas afirmar que contiene todas las lecturas. En un modelo de componentes, esa unidad es el componente entero: la única garantía disponible es que todo lo que el componente leyó, lo leyó dentro de sí.
Un motor que rastrea dependencias por lectura puede ser tan fino como el grano de sus nodos reactivos. Si cada expresión de la plantilla es un efecto propio con su propia lista de fuentes, la invalidación es por expresión.
De aquí sale la implicación que ordena el resto del nivel: la granularidad es una consecuencia del tracking, no una elección independiente. Por eso ningún framework de la familia 1 puede volverse fino sin adoptar tracking, y por eso adoptar tracking es exactamente lo que hizo la familia 2.
Un motor puede invalidar grueso y actualizar fino, y eso es justamente lo que hace la reconciliación por comparación: invalida el componente entero —lo reejecuta— pero luego compara y aplica al DOM solo lo que cambió. Es un detalle importante para ser justos con la familia 1, porque el trabajo sobre el DOM, que es el caro, ya es fino. Lo que no es fino es el trabajo en JavaScript previo a la comparación.
La granularidad efectiva la escribe el programador
Aquí está el matiz que más se ignora. Un framework fija la granularidad máxima alcanzable, no la que tu aplicación tiene de verdad.
En un motor de grano fino, si metes toda la lógica en un único memo enorme del que cuelga toda la interfaz, has convertido tu grafo en uno de grano grueso con la contabilidad de uno fino: lo peor de ambos. Es un patrón real y frecuente, sobre todo al portar código.
En un motor de grano grueso, si divides en muchos componentes pequeños y colocas el estado lo más abajo posible en el árbol, tu granularidad efectiva se acerca mucho a la fina. Este es el argumento serio de quien defiende que la diferencia importa menos de lo que se dice, y tiene bastante razón en el caso medio.
// Granularidad efectiva gruesa en un motor fino
const vista = memo(() => ({
cabecera: titulo(),
cuerpo: elementos().map(formatear),
pie: `${elementos().length} elementos`,
}));
// Cambiar el titulo reconstruye el cuerpo entero
// Granularidad efectiva fina: tres nodos independientes
const cabecera = memo(() => titulo());
const cuerpo = memo(() => elementos().map(formatear));
const pie = memo(() => `${elementos().length} elementos`);
Los dos fragmentos usan el mismo motor. El primero recalcula el mapeo completo cada vez que cambia el título; el segundo, no. La diferencia no la puso el framework: la puso quien escribió las llaves.
Si te quedas con una sola idea del nivel, que sea esta. La granularidad no se elige instalando un paquete: se decide en cada línea donde defines qué es un nodo. Y hay una consecuencia contraintuitiva que la mayoría descubre tarde. El grano óptimo no es el más fino posible, porque cada nodo cuesta memoria para su lista de observadores, cuesta las inserciones y borrados de sus aristas en cada reejecución, y cuesta un salto de indirección en cada lectura. Existe un tamaño de nodo por debajo del cual la contabilidad supera al cómputo ahorrado, y en cómputos triviales —una concatenación de dos cadenas, una comparación— ese umbral se cruza enseguida. Los motores fabricados con cuidado lo saben y no crean un nodo por cada expresión indiscriminadamente: el compilador de Solid, por ejemplo, solo genera un efecto para las expresiones de la plantilla que de verdad contienen una lectura reactiva, y las estáticas las emite como texto plano de una vez. La pregunta madura, entonces, no es cuán fino puede ser mi motor, sino dónde está el umbral en este cómputo concreto; y esa pregunta se responde midiendo, no leyendo comparativas.
El vocabulario que usaremos
Para el resto del track fijamos los términos, porque en la literatura se usan de forma inconsistente.
Grano grueso: la unidad de invalidación es una función de componente o mayor. El motor reejecuta esa función y compara el resultado.
Grano fino: la unidad de invalidación es un nodo reactivo individual, típicamente una expresión. El motor reejecuta solo ese nodo.
Reactividad de grano fino implica, además, que el sistema conoce el grafo y por tanto puede saltar directamente del cambio a las hojas afectadas sin recorrer la estructura intermedia. Ese salto directo es lo que las dos lecciones siguientes analizan por dentro, primero en un modelo y luego en el otro.
- Coge una pantalla real de una aplicación tuya y cuenta cuántos nodos reactivos o componentes se reejecutan al cambiar un solo valor de entrada.
- Compara ese número con el mínimo teórico: cuántas expresiones dependen realmente de ese valor.
- Identifica el nodo que agrupa más trabajo del necesario y divídelo en dos.
- Vuelve a contar y comprueba si la memoria y el número de aristas también cambiaron.