El coste asintótico real de cada modelo
Los órdenes de magnitud de las dos familias en actualización, arranque y memoria, con las constantes que las comparativas ocultan y los tres regímenes donde el ganador cambia.
Las comparativas de frameworks miden un caso concreto y lo presentan como una verdad general. Con las dos implementaciones de las lecciones anteriores delante podemos hacer algo más útil: escribir el coste de cada operación en función de las magnitudes que importan, ver dónde se cruzan las curvas, y entender por qué la misma pareja de motores puede dar resultados opuestos según qué midas.
- Escribir el coste de actualización, arranque y memoria de cada modelo.
- Identificar las magnitudes que gobiernan cada coste.
- Reconocer las constantes ocultas que dominan en tamaños realistas.
- Determinar en qué régimen se encuentra una aplicación concreta.
Las magnitudes
Para hablar con precisión necesitamos nombres. Llamemos N al número total de nodos de la vista, K al número de nodos afectados por un cambio concreto, D al número de dependencias —aristas— del grafo, y S al tamaño del subárbol que se reejecuta en el modelo de reconciliación.
Con N grande y K pequeño estamos en el caso que favorece al grano fino. Con K cercano a N —cambiar el conjunto de datos entero— los dos modelos convergen. Casi toda la discusión pública ignora que K es un parámetro y no una constante.
Los tres costes
Coste por actualización
| operación | reconciliación | grano fino |
|---|---|---|
| reejecutar lógica | O de S | O de K |
| producir descripción | O de S | no existe |
| comparar | O de S | no existe |
| mutar el mundo | O de K | O de K |
| mantener aristas | no existe | O de aristas de los K nodos |
La conclusión asintótica es directa: la reconciliación es O(S), el grano fino es O(K). Y como S es siempre mayor o igual que K —el subárbol reejecutado contiene por fuerza a los nodos que de verdad cambiaron—, el grano fino nunca es asintóticamente peor en actualización.
Lo que la tabla no dice es cuánto valen las constantes. Un nodo reejecutado en reconciliación cuesta una llamada, unas cuantas asignaciones de objeto y unas comparaciones de propiedad: son operaciones baratísimas sobre memoria contigua y bien predichas por el motor de JavaScript. Un nodo actualizado en grano fino cuesta una llamada, más el desatar y tejer de sus aristas, más varias operaciones sobre tablas hash. Con S igual a diez y K igual a ocho, el grano fino puede perder a pesar de tener mejor orden.
Como regla de campo que aguanta bien: el grano fino empieza a ganar de forma inequívoca cuando S supera a K en al menos un orden de magnitud. Por debajo de eso, las constantes deciden y hay que medir. Por eso una lista de diez mil filas con una modificada es una demostración devastadora, y un formulario de doce campos no distingue nada.
Coste de arranque
Aquí se invierten los papeles y se habla mucho menos de ello.
La reconciliación arranca en O(N): reejecuta todo, produce la descripción entera, y como no hay árbol anterior aplica todos los parches como creaciones. No mantiene ninguna estructura persistente más allá del árbol resultante.
El grano fino arranca en O(N + D): crea los nodos del DOM, y además crea un objeto reactivo por cada punto variable, ejecuta cada uno una primera vez y teje todas sus aristas. El término D no aparece en el otro modelo en absoluto.
En pantallas grandes de primera carga esto es medible y va en contra del grano fino. Que en la práctica no se note tanto se debe a un factor externo: los motores de grano fino suelen ir acompañados de compiladores que generan el DOM por clonación de plantillas en lugar de por llamadas individuales, lo que ahorra en el término N lo que pierden en el D. Es una ventaja de la herramienta, no del modelo reactivo.
Coste de memoria
La reconciliación mantiene el árbol descriptivo anterior: O(N) objetos, que además son basura de vida corta muy amigable para el recolector generacional.
El grano fino mantiene el grafo: O(N + D) objetos, de vida larga, que sobreviven a las colecciones jóvenes y acaban en el espacio antiguo. La diferencia no está solo en el tamaño sino en el perfil: memoria persistente que hay que trazar en cada ciclo de marcado, frente a memoria efímera que se descarta casi gratis.
flowchart TB subgraph reconciliacion A1[memoria efimera por ciclo] --> A2[recolector joven barato] end subgraph granofino B1[grafo persistente] --> B2[recolector viejo mas caro] B1 --> B3[coste por arista mantenida] end style A1 fill:#89b4fa,color:#11111b style A2 fill:#a6e3a1,color:#11111b style B1 fill:#cba6f7,color:#11111b style B2 fill:#f9e2af,color:#11111b style B3 fill:#f9e2af,color:#11111b
Los tres regímenes
Con las tres tablas se pueden delimitar tres regímenes donde el ganador es distinto, y en cuál estés depende de tu aplicación, no de tus preferencias.
Régimen de actualización intensiva. N grande, K pequeño, muchas actualizaciones por sesión. Paneles de datos en vivo, editores, visualizaciones, juegos. El grano fino gana claramente y la ventaja crece con N. El coste de arranque se amortiza en los primeros segundos.
Régimen de arranque dominante. N grande, pocas actualizaciones, la métrica que importa es el tiempo hasta interactivo. Páginas de contenido, comercio electrónico, cualquier cosa donde el usuario llega, mira y se va. El grano fino paga el grafo y no lo amortiza; la ventaja, si la hay, viene del compilador y no del modelo.
Régimen de reemplazo masivo. K cercano a N: cambiar de página, cargar un conjunto de datos nuevo, filtrar de mil a tres elementos. Los dos modelos hacen esencialmente el mismo trabajo, y el grano fino paga además desatar y volver a tejer todas las aristas afectadas. Aquí la reconciliación puede ganar, y es el caso que ninguna comparativa mide.
La conversación sobre modelos reactivos casi siempre compara medias, y las medias son irrelevantes para la experiencia percibida. Lo que un usuario nota no es que la actualización media tarde ocho milisegundos en lugar de doce: nota el fotograma de ciento veinte milisegundos que aparece una vez cada tanto. Y las dos familias tienen perfiles de cola radicalmente distintos. La reconciliación produce un coste proporcional al subárbol invalidado, con una varianza acotada: nunca es gratis, nunca es catastrófico, y su peor caso es predecible. El grano fino produce un coste casi nulo la mayor parte del tiempo, con picos ocasionales cuando una escritura cae en un nodo del que cuelga media aplicación y provoca la reconstrucción de todo un subgrafo, incluida la de sus aristas. La distribución del grano fino tiene mejor mediana y peor cola. Esto tiene una consecuencia operativa concreta: si tu métrica es INP —que es un percentil alto por definición— optimizar el caso medio del grano fino puede no mover la aguja en absoluto, mientras que localizar y partir el único nodo responsable del pico sí la mueve entera. La herramienta correcta para eso no es una comparativa de frameworks: es un perfilador sobre tu propio grafo, buscando el nodo con más descendientes.
Cómo situar tu aplicación
Tres preguntas bastan para clasificarte, y las tres se responden midiendo.
Cuántas actualizaciones ocurren por sesión frente a cuántos nodos se crean. Si la proporción es de diez a uno o mayor, estás en el primer régimen.
Cuál es el valor típico de K dividido entre S. Si el subárbol que se invalida es mucho mayor que lo que de verdad cambia, hay margen; si son parecidos, no lo hay.
Cuánto de tu presupuesto de tiempo está en producir la vista y cuánto en el layout y el pintado del navegador. Si el segundo domina —y en interfaces con mucho CSS domina más de lo que la gente cree—, cambiar de modelo reactivo no moverá nada, porque ese coste lo pagan los dos por igual.
- Implementa la misma lista con los dos motores de las lecciones anteriores.
- Mide el tiempo de una actualización para
Nigual a diez, cien, mil y diez mil, conKfijo en uno. - Localiza el
Na partir del cual el grano fino gana e interpreta la constante que estabas pagando por debajo. - Repite con
Kigual aNy comprueba cuál gana ahora.