Inicio / Artículos / Medición de los gestores de estado de React: comparación de conteos de renders y costo del paquete.

Medición de los gestores de estado de React: comparación de conteos de renders y costo del paquete.

Una aplicación de carritos desarrollada con ocho patrones de estado de React, analizada en cuanto a renders innecesarios y tamaño del paquete comprimido, y lo que indican las cifras sobre Zustand, Valtio y Context.

2227 palabras

La pregunta sobre qué gestor de estado de React elegir surge constantemente, y el debate suele basarse en opiniones más que en mediciones. Un enfoque más útil es desarrollar la misma funcionalidad básica con cada patrón común, mantener el comportamiento constante mediante un conjunto de pruebas compartido, y contar qué es lo que realmente difiere: con qué frecuencia se vuelve a renderizar los componentes y cuántos kilobytes añade cada opción. Este artículo explica un experimento de este tipo con ocho patrones, detalla por qué surgen los números que se obtienen y ofrece una receta reproducible para realizar la misma comparación con su propio estado.

Cómo se configura el experimento

La aplicación de prueba es deliberadamente sencilla: un carrito de compras con un botón de manzana, un botón de plátano, un total en tiempo real y una insignia de tema cuyo valor nunca cambia. Cada implementación se somete a la misma prueba de comportamiento, que hace clic en manzana tres veces y en plátano dos veces, verificando que los resultados sean idénticos. Mientras se ejecuta la prueba, un contador registra cada renderizado de cada componente. Por separado, esbuild mide cuánto añade cada solución a un paquete minificado y comprimido, excluyendo a React ya que todas las opciones generan el mismo costo.

Los ocho competidores son:

  • lifted useState transmitido a través de props
  • Contexto combinado con useReducer
  • Redux Toolkit
  • Zustand
  • Jotai
  • Valtio
  • MobX
  • Un almacén escrito a mano basado en useSyncExternalStore, sin dependencias

Todas las ocho cumplen con el contrato común, por lo que cualquier diferencia se refiere al costo y no a la corrección:

✓ lifted useState        › passes the shared cart contract
✓ Context + useReducer   › passes the shared cart contract
✓ Redux Toolkit          › passes the shared cart contract
✓ Zustand                › passes the shared cart contract
✓ Jotai                  › passes the shared cart contract
✓ Valtio                 › passes the shared cart contract
✓ MobX                   › passes the shared cart contract
✓ useSyncExternalStore   › passes the shared cart contract

Tests  8 passed (8)

Aquí se enumeran las versiones de la biblioteca y el entorno de ejecución utilizados para las mediciones, junto con el repositorio que alberga las ocho implementaciones, la prueba compartida y ambos scripts de medición:

Tested with react@19.2.8, zustand@5.0.15, @reduxjs/toolkit@2.12.0, react-redux@9.3.0,
jotai@2.20.2, valtio@2.3.2, mobx@7.0.3, mobx-react-lite@5.0.3, node 22.
Repo: github.com/noorjsdivs/state-patterns — 8 implementations, 1 shared test, 2 measurements.

Dos opciones para mantener una comparación justa

Ambas provienen de errores que aparecen en aplicaciones reales, y no de las normas de pruebas de rendimiento.

Primero, cada implementación crea su almacenamiento dentro de un componente en lugar de a nivel de módulo. De esta manera, cada aplicación iniciada comienza realmente desde cero, y no hay fugas de estado entre las ejecuciones de prueba.

Segundo, la insignia del tema existe únicamente como un elemento pasivo. Se suscribe a un valor que nunca se modifica al hacer clic, por lo que cualquier renderizado que realice durante la prueba es un trabajo inútil cuya culpa solo puede atribuirse al patrón de estado.

Qué está deliberadamente fuera del alcance

El concurso abarca únicamente el estado compartido del cliente. TanStack Query no está incluido porque gestiona un caché de datos del servidor, lo cual es un problema distinto con reglas diferentes. El estado de la URL no está incluido porque la barra de direcciones pertenece al enrutador. Es probable que su aplicación necesite ambos, pero ninguno compite en este evento en particular.

Sorpresa número uno: el tamaño del código es casi idéntico

Antes de los números interesantes, hay uno aburrido que merece atención. Las ocho implementaciones varían entre 39 y 58 líneas de código. La opción más compleja, Redux Toolkit con 58 líneas, es solo diecinueve líneas más larga que el enfoque más simple posible, lifted state con 39 líneas. A esta escala, el argumento sobre el código genérico que domina tantas discusiones sobre gestión de estado se reduce a diecinueve líneas. Las diferencias reales están en otro lugar y solo se vuelven visibles con herramientas de medición.

El marcador de rendimiento

Aquí se muestra cuántas veces se renderizó cada componente durante los cinco clics, incluyendo el montaje inicial:

5 clicks (3 apple, 2 banana)     Apple  Banana  Total  Theme(idle)

lifted useState                    6      6       6       6
Context + useReducer               6      6       6       6
Redux Toolkit                      4      3       6       1
Zustand                            4      3       6       1
MobX                               4      3       6       1
useSyncExternalStore               4      3       6       1
Jotai                              5      4       7       2
Valtio                             2      2       2       1

De estas columnas surgen tres historias distintas.

Lifted state y Context vuelven a renderizar todo

Con el estado elevado mediante useState y un único Context, cada componente se renderiza con cada clic. La insignia del tema se mostró seis veces aunque su valor nunca cambió, y el botón de plátano se renderizó con cada clic en la manzana. Este es el mecanismo concreto detrás de la advertencia común de que Context no es un gestor de estado. useContext suscribe a un componente al valor completo del contexto, por lo que cada vez que ese valor cambia de identidad, todos los componentes consumidores se renderizan. Colocar todo tu estado en un único contexto equivale, en efecto, a tener un estado elevado con una capa adicional.

La solución habitual es dividir el estado en varios contextos o memorizar a los componentes consumidores, pero esto no se midió aquí. La fila refleja cómo se escribe Context con mayor frecuencia: un proveedor que alberga un único valor.

Cuatro APIs, un resultado idéntico

Redux Toolkit, Zustand, MobX y el almacén escrito a mano producen exactamente la misma columna: cada componente se renderiza una vez al montarse y nuevamente solo cuando realmente cambia el fragmento de estado que lee. Sus APIs no se parecen en nada, pero su comportamiento en tiempo de ejecución es idéntico, ya que los cuatro se basan en la misma idea fundamental. Los componentes escuchan un fragmento específico del estado en lugar de todo el almacén, y solo reciben notificaciones cuando ese fragmento cambia. Para conocer más detalles sobre cómo funciona esa selección y la comprobación de igualdad en una de estas bibliotecas, consulte cómo React Redux decide cuándo volver a renderizar.

Números sospechosamente bajos de Valtio

A primera vista, la columna de Valtio parece un contador roto: dos actualizaciones para un botón que se hizo clic tres veces. Las cifras son correctas. Valtio agrupa las notificaciones de cambios en la cola de microtareas, por lo que varias actualizaciones rápidas y síncronas se reducen a una sola actualización por componente, y los valores finales siguen siendo correctos.

Hay una advertencia importante. Una persona que hace clic a velocidad normal genera una actualización por cada clic, ya que cada clic termina antes de que comience el siguiente. La ventaja solo se aprecia cuando las actualizaciones llegan en ráfagas, como ocurre con los mensajes de WebSocket, los datos en streaming o los eventos de arrastre. Si su aplicación tiene ese perfil, esta columna merece una atención detallada.

La actualización adicional constante de Jotai

Jotai renderizó cada componente una vez más que el grupo basado en selectores, incluyendo dos renderizaciones para la insignia del tema inactivo. Su nivel de detalle es correcto, ya que los componentes siguen reaccionando únicamente a los elementos que utilizan, y el renderizado adicional es uniforme en todas las columnas. Ese patrón indica algún problema en la secuencia inicial de montaje con el proveedor Store y no una fuga de suscripciones, pero la causa exacta no se identificó. Considérelo como una pregunta abierta en lugar de un veredicto en contra de Jotai.

Análisis del tamaño del paquete

Esto es lo que cada patrón agrega a un paquete comprimido, contando tanto el código propio del patrón como su biblioteca, tratando a React como externo:

lifted useState          0.4 KB
useSyncExternalStore     0.5 KB     (zero dependencies)
Context + useReducer     0.5 KB
Zustand                  0.7 KB
Valtio                   2.7 KB
Jotai                    4.4 KB
Redux Toolkit           10.7 KB
MobX                    13.2 KB

La opción más pesada es 33 veces mayor que la más ligera. En otras palabras, Redux Toolkit y MobX juntos suman 23,9 KB, mientras que los seis patrones restantes en conjunto totalizan 9,2 KB.

Lo interesante es cómo esta lista interactúa con el marcador de rendimiento. Solo dos patrones combinan un comportamiento de renderizado perfecto con un tamaño inferior a un kilobyte: Zustand y el almacén escrito a mano. Redux Toolkit logra la misma columna de renderizado con 10,7 KB, y MobX con 13,2 KB. Eso no es necesariamente un defecto en ellos; es simplemente un costo, y ese costo solo tiene sentido una vez que se sabe qué se obtiene a cambio.

Tres opciones prácticas y su costo respectivo

Para el estado del cliente compartido en una aplicación típica, las mediciones indican tres herramientas que vale la pena considerar.

Zustand como opción por defecto

Zustand ofrece una granularidad de renderizado perfecta con 0,7 KB y solo 54 líneas de código; además, su API casi no requiere explicaciones para un nuevo miembro del equipo: se trata de un hook que acepta un selector. En esta prueba, su rendimiento fue similar al de Redux, pero con aproximadamente una quinta parte del tamaño.

Ese resultado depende de una sola disciplina: siempre se debe seleccionar un fragmento específico. Una llamada como useCart(s => s) suscribe al componente a todo el almacén, lo que reproduce el problema del Context. La solución eficiente radica en escribir selectores más específicos, no en el nombre de la biblioteca. Si un selector devuelve un nuevo objeto o array en cada llamada, también se necesita una función auxiliar para la igualdad superficial, de lo contrario cada actualización parecerá un cambio.

Un almacén useSyncExternalStore escrito a mano

Esta opción es la que hace que el experimento sea más atractivo. La implementación completa, incluido el almacén, cabe en 58 líneas, añade 0.5 KB, coincide exactamente con la columna de renderizado de Redux y no presenta dependencias que requieran auditoría o actualización. useSyncExternalStore es la herramienta primitiva que proporciona React mismo para suscribirse a almacenes externos de forma segura, incluso durante el renderizado concurrente. Para los autores de bibliotecas o para equipos que prefieren dependencias mínimas, es suficiente. La contrapartida es que usted es responsable del código: los herramientas de desarrollo, el middleware y la persistencia son algo que debe crear usted mismo si los necesita.

Valtio para actualizaciones intermitentes

El agrupamiento de microtareas de Valtio era medible, real y único entre estos competidores, y 2,7 KB es un precio razonable por ello. Escribir una mutación directa como state.apples++ y ver que se actualizan exactamente los componentes adecuados resulta casi demasiado conveniente, pero los conteos de renderizado confirman que el comportamiento es genuino.

Por qué los demás pierden esta competencia sin ser malos

La estructura de la prueba es importante, ya que favorece un estado pequeño y plano. Las otras opciones tienen ventajas que esta aplicación no puede aprovechar:

  • Los 10,7 KB de Redux Toolkit se justifican por las herramientas de desarrollo, la depuración con viajes en el tiempo, los middleware y las convenciones que funcionan bien en grandes organizaciones. Con cincuenta desarrolladores trabajando en una misma base de código, esas convenciones son el verdadero producto.
  • El modelo observable de MobX es más fuerte en código de dominio con muchas clases, algo que esta aplicación no tiene.
  • El gráfico atómico de Jotai brilla cuando el estado derivado se vuelve profundo e interconectado, y un total derivado único apenas lo alcanza.
  • El contexto sigue siendo la herramienta adecuada para valores que rara vez cambian, como el tema, la configuración regional o la sesión. Irónicamente, la columna del tema es precisamente donde tuvo el peor rendimiento aquí, ya que el estado del carrito compartía su proveedor.
  • El estado elevado sigue siendo la opción correcta para estados que no se comparten. Solo perdió porque este concurso trata específicamente del compartir.
  • Una regla de diseño que no es cuestión de gusto

    Cada implementación aquí creó su almacén dentro de un componente. Un almacén definido a nivel de módulo sobrevive al desmontaje, y así es como un carrito de compras persiste tras el cierre de sesión y da la bienvenida a la siguiente persona que inicia sesión en el mismo dispositivo. También provoca que el estado se filtre entre pruebas y entre solicitudes durante la renderización en servidor. Si solo toma una regla de revisión de código de esta comparación, que sea esta: limite los almacenes al árbol de componentes, generalmente creándolos en un proveedor, a menos que quiera intencionalmente un ciclo de vida global.

    Ejecutar el mismo concurso en su propia aplicación

    Puede reproducir esta medición para su propio estado en una tarde:

    1. Elija el elemento de estado compartido más debatido en su aplicación y construya un conjunto de cuatro componentes alrededor de él: dos componentes que escriben, uno que lee un valor derivado y otro que actúa como observador pasivo.
  • Agregue un contador de renderizado a cada componente. Un objeto a nivel de módulo más una llamada a track() en el cuerpo de cada componente requiere unas diez líneas de código. El recuento del “espectador” representa el costo del Context, medido con precisión en lugar de estimado.
  • Mida cada candidato utilizando esbuild --bundle --minify, indicando que React es un archivo externo. El proceso tarda unos segundos y agrega una columna con el tamaño en kilobytes a los resultados.
  • Si está utilizando Context y el “espectador” vuelve a renderizar, divida el contexto o pase a utilizar un almacén basado en selectores; luego ejecute nuevamente el contador e incluya las cifras antes y después en su pull request.
  • Repita todo el proceso una vez que React Compiler forme parte de su proceso de compilación, ya que la memorización automática puede modificar significativamente la fila correspondiente al estado compartido.
  • Preguntas abiertas

    Dos hilos siguen sin estar fijados. El más pequeño es la causa de la renderización adicional en Jotai. El más grande es React Compiler. La fila lifted-state muestra 6/6/6/6 precisamente porque nada en ella está memorizado, y la memorización es exactamente lo que automatiza el compilador. Ver si esa fila se alinea con los almacenes basados en selectores en código de producción real, y no solo en una demostración, es el siguiente experimento obvio.

    Conclusiones clave

    • A pequeña escala, las diferencias en el código genérico entre las bibliotecas de estado son insignificantes; es en el comportamiento de renderización y el tamaño del paquete donde realmente difieren.
    • Un único Contexto que almacena estado cambiante vuelve a renderizar a todos los consumidores, incluidos componentes que nunca leen el valor modificado.
    • Los almacenes basados en selectores, ya sea Redux Toolkit, Zustand, MobX o un almacén personalizado con useSyncExternalStore, convergen en el mismo patrón de renderización eficiente.
  • Zustand y una tienda escrita a mano permiten ese comportamiento con un tamaño inferior a un kilobyte; las bibliotecas más pesadas ganan en tamaño gracias a herramientas y convenciones, no al proceso de renderizado.
  • El agrupamiento de operaciones en Valtio resulta útil para flujos de actualización intermitentes en lugar de clics normales.
  • Cree tiendas dentro del árbol de componentes para que el estado no sobreviva a la sesión que lo generó.